Как записать денормализованные данные в 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 для слушателя, но я также не использую транзакцию.

Если мы хотим безопасно выполнить этот тип операции с клиента, нам потребуется:

  1. правила безопасности, обеспечивающие совпадение имен в обоих местах. Но правила должны обеспечивать достаточную гибкость, чтобы они временно отличались, пока мы меняем название. Так что это превращается в довольно болезненную схему двухфазного коммита.
    1. изменить все username поля для сообщений so:209103 в null (какая-то магическая ценность)
    2. изменить name пользователя so:209103 "пух"
    3. изменить username в каждом сообщении so:209103 то есть null в puf,
    4. этот запрос требует and двух условий, которые запросы Firebase не поддерживают. Таким образом, мы получим дополнительную собственность uid_plus_name (со значением so:209103_puf) что мы можем запросить.
  2. код на стороне клиента, который обрабатывает все эти переходы транзакционно.

При таком подходе у меня болит голова. И обычно это означает, что я делаю что-то не так. Но даже если это правильный подход, у меня болит голова, и я гораздо чаще допускаю ошибки в коде. Поэтому я предпочитаю искать более простое решение.

Возможная последовательность

Обновление (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. Функции запускаются всякий раз, когда изменяется основное значение (например, имя пользователя), а затем распространяют изменения в денормализованных полях.

Это не так быстро, как транзакция, но во многих случаях это не обязательно.

Другие вопросы по тегам