2015-06-07 12 views
24

Ho letto i documenti Firebase su Stucturing Data. L'archiviazione dei dati è economica, ma il tempo dell'utente non lo è. Dovremmo ottimizzare per ottenere operazioni e scrivere in più posti.Come scrivere i dati denormalizzati in Firebase

Allora ho potrebbe memorizzare un elenco nodo e un nodo lista-index, con alcuni dati duplicati tra i due, a meno il nome della lista.

Sto usando ES6 e prometto nella mia app javascript di gestire il flusso asincrono, principalmente di recuperare una chiave di ref dalla base di fuoco dopo il primo push di dati.

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); 
}); 

Come posso assicurarsi che i dati rimane in sincronia in tutti i luoghi, conoscendo la mia app viene eseguito solo sul client?

Per controllo di integrità, ho impostato un setTimeout nella mia promessa e chiusi browser prima che risolta, e in effetti il ​​mio database non era più coerente, con un indice di aggiuntivo salvato senza un corrispondente lista.

Qualche consiglio?

+3

* Scriverò una risposta corretta di seguito. * Questa prima frase mi rende felice. Puoi ripeterlo alle feste di compleanno? Soprattutto quando sono presenti gli amici dev. :-) –

+0

questo sembra rilevante, anche .. https://stackoverflow.com/questions/47334382 – Fattie

risposta

47

Ottima domanda. Conosco tre approcci a questo, che elencherò qui di seguito.

Prenderò un esempio leggermente diverso, soprattutto perché consente di utilizzare termini più concreti nella spiegazione.

Supponiamo di avere un'applicazione di chat, in cui vengono archiviate due entità: messaggi e utenti. Nella schermata in cui mostriamo i messaggi, mostriamo anche il nome dell'utente. Quindi, per ridurre al minimo il numero di letture, memorizziamo il nome dell'utente anche con ogni messaggio di 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" 

Così abbiamo memorizzare la copia principale del profilo dell'utente nel nodo users. Nel messaggio memorizziamo il uid (così: 209103 e così: 3648524) in modo che possiamo cercare l'utente. Ma noi anche memorizziamo il nome dell'utente nei messaggi, in modo che non dobbiamo cercare questo per ogni utente quando vogliamo visualizzare un elenco di messaggi.

Così ora cosa succede quando vado alla pagina Profilo del servizio chat e cambio il mio nome da "Frank van Puffelen" a solo "puf".

aggiornamento transazionale

Esecuzione di un aggiornamento transazionale è quello che, probabilmente, si apre alla mente della maggior parte degli sviluppatori inizialmente. Vogliamo sempre il username nei messaggi per corrispondere allo name nel profilo corrispondente.

Utilizzando multipath scrive (aggiunto il 20.150.925)

Dal Firebase 2.3 (per JavaScript) e 2,4 (per Android e iOS), è possibile ottenere aggiornamenti atomiche abbastanza facilmente utilizzando un singolo aggiornamento multipath:

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); 
    }); 
} 

Questo invierà un singolo comando aggiornamento per Firebase che aggiorna il nome dell'utente nel proprio profilo e in ogni messaggio.

precedente approccio atomico

Così, quando il cambio utente è il name nel loro profilo:

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 */); 

Abbastanza coinvolti e il lettore attento avrà notato che ho imbrogliato nella gestione dei messaggi. Il primo cheat è che non ho mai chiamato off per il listener, ma non uso una transazione.

Se vogliamo fare in modo sicuro questo tipo di operazione da parte del cliente, avremmo bisogno:

  1. regole di sicurezza che garantiscono i nomi in entrambi i luoghi partita. Ma le regole devono consentire sufficiente flessibilità per loro di essere temporaneamente diverse mentre stiamo cambiando il nome. Quindi questo si trasforma in uno schema di commit a due fasi piuttosto doloroso.
    1. cambiamento tutte username i campi per i messaggi di so:209103 a null (qualche valore magico)
    2. cambiamento del name di utente so:209103 a 'puf'
    3. cambiamento del username in ogni messaggio per so:209103 che è null a puf.
    4. la query richiede uno and di due condizioni, che le query Firebase non supportano. Quindi finiremo con una proprietà aggiuntiva uid_plus_name (con valore so:209103_puf) su cui possiamo eseguire una query.
  2. codice lato client che gestisce tutte le transizioni a livello di transazione.

Questo tipo di approccio mi fa male alla testa. E di solito significa che sto facendo qualcosa di sbagliato. Ma anche se è l'approccio giusto, con una testa che mi fa male ho più possibilità di commettere errori di programmazione. Quindi preferisco cercare una soluzione più semplice.

coerenza eventuale

Update (20.150.925): Firebase rilasciato una funzione per consentire scritture atomiche a percorsi multipli. Funziona in modo simile all'approccio di seguito, ma con un singolo comando. Vedere la sezione aggiornata sopra per leggere come funziona.

Il secondo approccio dipende dalla divisione dell'azione dell'utente ("Voglio cambiare il mio nome in" puf "") dalle implicazioni di tale azione ("Abbiamo bisogno di aggiornare il nome nel profilo così: 209103 e in ogni messaggio che ha user = so:209103)

mi piacerebbe gestire la ridenominazione in uno script che si corre su un server il metodo principale sarebbe qualcosa di simile:..

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 }); 
    }) 
    }); 
} 

ancora una volta mi prendo un paio di scorciatoie qui, come usare once('value' (che in generale è una cattiva idea per prestazioni ottimali con Firebase). Ma nel complesso l'approccio è più semplice, a Il costo di non avere tutti i dati completamente aggiornati allo stesso tempo. Ma alla fine i messaggi verranno aggiornati per corrispondere al nuovo valore.

fregandosene

Il terzo approccio è il più semplice di tutti: in molti casi non si hanno veramente a aggiornare i dati duplicato a tutti. Nell'esempio che abbiamo usato qui, si potrebbe dire che ogni messaggio ha registrato il nome come l'ho usato in quel momento. Non ho cambiato il mio nome fino ad ora, quindi ha senso che i vecchi messaggi mostrino il nome che ho usato in quel momento. Ciò si applica in molti casi in cui i dati secondari sono di natura transazionale. Ovviamente non si applica ovunque, ma dove si applica "non importa" è l'approccio più semplice di tutti.

Sommario

Mentre il sopra sono solo ampie descrizioni di come si potrebbe risolvere questo problema e non sono assolutamente completa, trovo che ogni volta che ho bisogno di ventaglio dati duplicati si ritorna a uno di questi base approcci.

+1

Grazie Frank per l'ottima risposta! In effetti, sono andato con l'approccio "Non preoccuparti". Ho fatto passare le mie operazioni di scrittura in modo che l'elemento venga prima dell'indice, quindi non rischio di avere un elemento nell'elenco che si collega a qualche parte senza dati (ma ora potrei finire con un elemento dell'elenco orfano, che non è un grande affare). – legolandbridge

+0

Una grande risposta. Il consiglio che hai delineato è super utile e si applica a molti scenari, non solo a Firebase. –

+1

Attenzione a fare in modo di ritardare l'attività del server in * consistenza finale * e controllare che il nome non sia stato cambiato di nuovo. – AJcodez

2

Per aggiungere a Franks un'ottima risposta, ho implementato l'approccio di coerenza finale con un set di Firebase Cloud Functions. Le funzioni vengono attivate ogni volta che viene modificato un valore principale (ad esempio, nome utente) e quindi propagano le modifiche ai campi denormalizzati.

Non è veloce come una transazione, ma per molti casi non è necessario.

+1

Grande aggiunta Uffe. Le Cloud Functions sono ottime per questo, a patto che non si abbiano requisiti rigorosi in tempo reale o offline. –