Подтвердить что ты не робот

Как написать денормализованные данные в Firebase

Я прочитал документы Firebase в Stucturing Data. Хранение данных дешево, но пользовательское время - нет. Мы должны оптимизировать для получения операций и писать в нескольких местах.

Итак, я могу сохранить список node и list-index node, с некоторыми дублируемыми данными между ними, по крайней мере, именем списка.

Я использую ES6 и promises в своем приложении javascript для обработки потока async, в основном, извлечения ключа 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 в своем обещании и закрыл свой браузер до его разрешения, и действительно, моя база данных больше не была согласованной, а дополнительный индекс был сохранен без соответствующего списка.

Любые советы?

4b9b3361

Ответ 1

Отличный вопрос. Я знаю три подхода к этому, которые я приведу ниже.

Я приведу несколько другой пример для этого, главным образом потому, что он позволяет мне использовать более конкретные термины в объяснении.

Скажем, у нас есть приложение для чата, где мы храним два объекта: сообщения и пользователи. На экране, где мы показываем сообщения, мы также показываем имя пользователя. Таким образом, чтобы свести к минимуму количество чтений, мы также сохраняем имя пользователя с каждым сообщением чата.

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 node. В сообщении мы сохраняем uid (так: 209103 и так: 3648524), чтобы мы могли искать пользователя. Но мы также сохраняем имя пользователя в сообщениях, поэтому нам не нужно искать это для каждого пользователя, когда мы хотим отобразить список сообщений.

Итак, теперь, когда я перехожу на страницу профиля в службе чата и меняю свое имя от "Frank van Puffelen" до "puf".

Обновление транзакции

Выполнение транзакционного обновления - это тот, который, вероятно, появляется на ум у большинства разработчиков. Мы всегда хотим, чтобы 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 data: ", snapshot.val());
}, false /* don't apply the change locally */);

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

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

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

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

Конечная согласованность

Обновление (20150925). Firebase выпустила функцию, позволяющую выполнять атомарную запись по нескольким путям. Это похоже на подход ниже, но с одной командой. См. Обновленный раздел выше, чтобы прочитать, как это работает.

Второй подход зависит от разделения действия пользователя ( "Я хочу изменить свое имя на" puf ") из последствий этого действия (" Нам нужно обновить имя в профиле так: 209103 и в каждом сообщении, которое имеет user = so:209103).

Я бы обработал переименование в script, который мы запускаем на сервере. Основной метод будет примерно таким:

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

Не заботясь

Третий подход - самый простой из всех: во многих случаях вам вообще не нужно обновлять дублированные данные. В примере, который мы использовали здесь, вы могли бы сказать, что каждое сообщение записало это имя, когда я использовал его в то время. Я не изменял свое имя до сих пор, поэтому имеет смысл, что более старые сообщения показывают имя, которое я использовал в то время. Это применимо во многих случаях, когда вторичные данные являются транзакционными по своей природе. Разумеется, это не применяется повсеместно, но там, где оно применяется, "не заботясь" - это самый простой подход для всех.

Резюме

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

Ответ 2

Чтобы добавить к большому ответу Фрэнкса, я реализовал возможный подход согласованности с набором Firebase Cloud Functions. Функции запускаются при каждом изменении первичного значения (например, имени пользователя), а затем распространяются изменения в денормализованные поля.

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