Como escrever dados desnormalizados no Firebase

Li os documentos do Firebase sobre dados de estruturação . O armazenamento de dados é barato, mas o horário do usuário não é. Devemos otimizar para obter operações e escrever em vários lugares.

Então eu poderia armazenar um nó de lista e um nó de índice de lista , com alguns dados duplicados entre os dois, pelo menos o nome da lista.

Estou usando ES6 e prometo em meu aplicativo de JavaScript para lidar com o stream asynchronous, principalmente de buscar uma chave ref do firebase após o primeiro envio de dados.

let addIndexPromise = new Promise( (resolve, reject) => { let newRef = ref.child('list-index').push(newItem); resolve( newRef.key()); // ignore reject() for brevity }); addIndexPromise.then( key => { ref.child('list').child(key).set(newItem); }); 

Como faço para garantir que os dados permaneçam em sincronia em todos os lugares , sabendo que meu aplicativo é executado apenas no cliente?

Para verificação de integridade, eu defini um setTimeout na minha promise e fechei meu navegador antes de resolvê-lo, e de fato meu database não estava mais consistente, com um índice extra salvo sem uma lista correspondente .

Algum conselho?

Ótima pergunta. Eu sei de três abordagens para isso, que vou listar abaixo.

Vou dar um exemplo um pouco diferente disso, principalmente porque me permite usar termos mais concretos na explicação.

Digamos que temos um aplicativo de bate-papo, onde armazenamos duas entidades: mensagens e usuários. Na canvas onde mostramos as mensagens, mostramos também o nome do usuário. Então, para minimizar o número de leituras, também armazenamos o nome do usuário com cada mensagem de chat.

 users so:209103 name: "Frank van Puffelen" location: "San Francisco, CA" questionCount: 12 so:3648524 name: "legolandbridge" location: "London, Prague, Barcelona" questionCount: 4 messages -Jabhsay3487 message: "How to write denormalized data in Firebase" user: so:3648524 username: "legolandbridge" -Jabhsay3591 message: "Great question." user: so:209103 username: "Frank van Puffelen" -Jabhsay3595 message: "I know of three approaches, which I'll list below." user: so:209103 username: "Frank van Puffelen" 

Portanto, armazenamos a cópia principal do perfil do usuário no nó dos users . Na mensagem nós armazenamos o uid (então: 209103 e assim: 3648524) para que possamos procurar o usuário. Mas também armazenamos o nome do usuário nas mensagens, para que não tenhamos que procurar por cada usuário quando quisermos exibir uma lista de mensagens.

Então, agora o que acontece quando eu vou para a página do perfil no serviço de bate-papo e mudo o meu nome de “Frank van Puffelen” para apenas “puf”.

Atualização transacional

Realizar uma atualização transacional é o que provavelmente vem à mente da maioria dos desenvolvedores inicialmente. Sempre queremos que o username nas mensagens corresponda ao name no perfil correspondente.

Usando gravações de caminhos múltiplos (adicionadas em 20150925)

Desde o Firebase 2.3 (para JavaScript) e o 2.4 (para Android e iOS), é possível obter atualizações atômicas com bastante facilidade usando uma única atualização de vários caminhos:

 function renameUser(ref, uid, name) { var updates = {}; // all paths to be updated and their new values updates['users/'+uid+'/name'] = name; var query = ref.child('messages').orderByChild('user').equalTo(uid); query.once('value', function(snapshot) { snapshot.forEach(function(messageSnapshot) { updates['messages/'+messageSnapshot.key()+'/username'] = name; }) ref.update(updates); }); } 

Isso enviará um único comando de atualização para o Firebase que atualizará o nome do usuário em seu perfil e em cada mensagem.

Abordagem atômica anterior

Então, quando o usuário muda o name em seu perfil:

 var ref = new Firebase('https://mychat.firebaseio.com/'); var uid = "so:209103"; var nameInProfileRef = ref.child('users').child(uid).child('name'); nameInProfileRef.transaction(function(currentName) { return "puf"; }, function(error, committed, snapshot) { if (error) { console.log('Transaction failed abnormally!', error); } else if (!committed) { console.log('Transaction aborted by our code.'); } else { console.log('Name updated in profile, now update it in the messages'); var query = ref.child('messages').orderByChild('user').equalTo(uid); query.on('child_added', function(messageSnapshot) { messageSnapshot.ref().update({ username: "puf" }); }); } console.log("Wilma's data: ", snapshot.val()); }, false /* don't apply the change locally */); 

Bastante envolvido e o leitor astuto notará que eu trapaceio no manuseio das mensagens. A primeira armadilha é que eu nunca desisto do ouvinte, mas também não uso uma transação.

Se quisermos fazer esse tipo de operação com segurança do cliente, precisaríamos:

  1. regras de segurança que garantem que os nomes em ambos os lugares correspondam. Mas as regras precisam permitir flexibilidade suficiente para que elas sejam temporariamente diferentes enquanto estamos mudando o nome. Então isso se transforma em um esquema de commit de duas fases bastante doloroso.
    1. altere todos os campos de username para mensagens em so:209103 para null (algum valor mágico)
    2. mudar o name do usuário so:209103 para ‘puf’
    3. mude o username em cada mensagem por so:209103 que é null para puf .
    4. Essa consulta exige uma and duas condições, que as consultas do Firebase não suportam. Então, vamos acabar com uma propriedade extra uid_plus_name (com valor so:209103_puf ) que podemos consultar.
  2. código do lado do cliente que manipula todas essas transições transacionalmente.

Esse tipo de abordagem faz minha cabeça doer. E geralmente isso significa que estou fazendo algo errado. Mas mesmo que seja a abordagem correta, com uma cabeça que dói, estou mais propenso a cometer erros de codificação. Então, prefiro procurar uma solução mais simples.

Consistência eventual

Atualização (20150925) : o Firebase lançou um recurso para permitir gravações atômicas em vários caminhos. Isso funciona de maneira semelhante à abordagem abaixo, mas com um único comando. Veja a seção atualizada acima para ler como isso funciona.

A segunda abordagem depende da divisão da ação do usuário (“Quero alterar meu nome para ‘puf'”) das implicações dessa ação (“Precisamos atualizar o nome no perfil para: 209103 e em cada mensagem que tenha user = so:209103 ).

Eu lidaria com a renomeação em um script que executamos em um servidor. O principal método seria algo assim:

 function renameUser(ref, uid, name) { ref.child('users').child(uid).update({ name: name }); var query = ref.child('messages').orderByChild('user').equalTo(uid); query.once('value', function(snapshot) { snapshot.forEach(function(messageSnapshot) { messageSnapshot.update({ username: name }); }) }); } 

Mais uma vez eu uso alguns atalhos aqui, como usar once('value' (o que geralmente é uma má ideia para um ótimo desempenho com o Firebase). Mas no geral a abordagem é mais simples, ao custo de não ter todos os dados completamente atualizados em ao mesmo tempo, mas eventualmente as mensagens serão todas atualizadas para corresponder ao novo valor.

Não se importar

A terceira abordagem é a mais simples de todas: em muitos casos, você não precisa realmente atualizar os dados duplicados. No exemplo que usamos aqui, você poderia dizer que cada mensagem registrou o nome como eu usei naquele momento. Eu não mudei meu nome até agora, então faz sentido que mensagens antigas mostrem o nome que eu usei naquele momento. Isso se aplica em muitos casos em que os dados secundários são de natureza transacional. Não se aplica em todos os lugares, é claro, mas onde se aplica “não se importar” é a abordagem mais simples de todas.

Resumo

Embora as descrições acima sejam apenas amplas sobre como você poderia resolver esse problema e elas definitivamente não estão completas, descubro que cada vez que preciso distribuir dados duplicados, ele volta para uma dessas abordagens básicas.

Para adicionar uma resposta excelente ao Franks, implementei a abordagem de consistência eventual com um conjunto de funções do Firebase Cloud . As funções são acionadas sempre que um valor primário (por exemplo, nome de usuário) é alterado e, em seguida, propaga as alterações para os campos desordenados.

Não é tão rápido quanto uma transação, mas em muitos casos não precisa ser.