Как записать денормализованные данные в Firebase
Я прочитал документы Firebase по структурированию данных. Хранение данных дешево, но время пользователя нет. Мы должны оптимизировать операции get и писать в нескольких местах.
Тогда я мог бы сохранить узел списка и узел списка-списка с некоторыми дублированными данными между ними, по крайней мере, именем списка.
Я использую ES6 и обещания в своем приложении javascript для обработки асинхронного потока, в основном для извлечения ключа ref из firebase после первого нажатия данных.
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);
});
Как мне обеспечить синхронизацию данных во всех местах, зная, что мое приложение работает только на клиенте?
Для проверки работоспособности я установил setTimeout в своем обещании и закрыл свой браузер, прежде чем он разрешился, и действительно, моя база данных больше не была согласованной, с дополнительным индексом, сохраненным без соответствующего списка.
Любой совет?
2 ответа
Отличный вопрос Мне известны три подхода к этому, которые я перечислю ниже.
Я приведу немного другой пример для этого, главным образом потому, что он позволяет мне использовать более конкретные термины в объяснении.
Скажем, у нас есть приложение чата, в котором мы храним две сущности: сообщения и пользователи. На экране, где мы показываем сообщения, мы также показываем имя пользователя. Таким образом, чтобы минимизировать количество операций чтения, мы сохраняем имя пользователя вместе с каждым сообщением чата.
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"
Таким образом, мы храним основную копию профиля пользователя в users
узел. В сообщении мы храним uid
(так:209103 и так:3648524), чтобы мы могли искать пользователя. Но мы также храним имя пользователя в сообщениях, поэтому нам не нужно искать его для каждого пользователя, когда мы хотим отобразить список сообщений.
Итак, что происходит, когда я захожу на страницу профиля в службе чата и меняю свое имя с "Фрэнк ван Пуффелен" на "пуф".
Транзакционное обновление
Первоначально, вероятно, приходит на ум выполнение транзакционного обновления для большинства разработчиков. Мы всегда хотим username
в сообщениях, чтобы соответствовать name
в соответствующем профиле.
Использование многопутевых записей (добавлено в 20150925)
Начиная с Firebase 2.3 (для JavaScript) и 2.4 (для Android и iOS), вы можете легко достичь атомарных обновлений, используя одно многопоточное обновление:
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);
});
}
Это отправит в Firebase одну команду обновления, которая обновляет имя пользователя в его профиле и в каждом сообщении.
Предыдущий атомный подход
Поэтому, когда пользователь меняет это name
в их профиле:
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 */);
Довольно вовлеченный, и проницательный читатель заметит, что я обманываю в обработке сообщений. Первый обман это то, что я никогда не звоню off
для слушателя, но я также не использую транзакцию.
Если мы хотим безопасно выполнить этот тип операции с клиента, нам потребуется:
- правила безопасности, обеспечивающие совпадение имен в обоих местах. Но правила должны обеспечивать достаточную гибкость, чтобы они временно отличались, пока мы меняем название. Так что это превращается в довольно болезненную схему двухфазного коммита.
- изменить все
username
поля для сообщенийso:209103
вnull
(какая-то магическая ценность) - изменить
name
пользователяso:209103
"пух" - изменить
username
в каждом сообщенииso:209103
то естьnull
вpuf
, - этот запрос требует
and
двух условий, которые запросы Firebase не поддерживают. Таким образом, мы получим дополнительную собственностьuid_plus_name
(со значениемso:209103_puf
) что мы можем запросить.
- изменить все
- код на стороне клиента, который обрабатывает все эти переходы транзакционно.
При таком подходе у меня болит голова. И обычно это означает, что я делаю что-то не так. Но даже если это правильный подход, у меня болит голова, и я гораздо чаще допускаю ошибки в коде. Поэтому я предпочитаю искать более простое решение.
Возможная последовательность
Обновление (20150925): Firebase выпустил функцию, позволяющую атомарную запись в несколько путей. Это работает аналогично подходу ниже, но с одной командой. Смотрите обновленный раздел выше, чтобы прочитать, как это работает.
Второй подход зависит от разделения действия пользователя ("Я хочу изменить свое имя на" puf "") от последствий этого действия ("Нам нужно обновить имя в профиле так:209103 и в каждом сообщении, которое имеет user = so:209103
).
Я бы обработал переименование в сценарии, который мы запускаем на сервере. Основной метод будет примерно таким:
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 });
})
});
}
Еще раз я сделаю несколько ярлыков здесь, таких как использование once('value'
(что в целом является плохой идеей для оптимальной производительности с Firebase). Но в целом подход проще, но не все данные обновляются одновременно. Но в конечном итоге все сообщения будут обновлены в соответствии с новым значением.
Не заботясь
Третий подход самый простой: во многих случаях вам вообще не нужно обновлять дублированные данные. В примере, который мы использовали здесь, вы могли бы сказать, что каждое сообщение записывало имя, которое я использовал в то время. Я не менял свое имя до сих пор, поэтому имеет смысл, что старые сообщения показывают имя, которое я использовал в то время. Это применяется во многих случаях, когда вторичные данные имеют транзакционный характер. Конечно, это применимо не везде, но там, где применяется "не забота", - самый простой подход.
Резюме
Хотя вышеизложенное является лишь подробным описанием того, как вы могли решить эту проблему, и они определенно не являются полными, я обнаружил, что каждый раз, когда мне нужно пролистать дублирующиеся данные, он возвращается к одному из этих основных подходов.
Чтобы добавить в Franks отличный ответ, я реализовал подход возможной согласованности с набором облачных функций Firebase. Функции запускаются всякий раз, когда изменяется основное значение (например, имя пользователя), а затем распространяют изменения в денормализованных полях.
Это не так быстро, как транзакция, но во многих случаях это не обязательно.