Ответ 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). Но в целом подход является более простым, за счет того, что не все данные полностью обновляются в одно и то же время. Но в итоге все сообщения будут обновлены в соответствии с новым значением.
Не заботясь
Третий подход - самый простой из всех: во многих случаях вам вообще не нужно обновлять дублированные данные. В примере, который мы использовали здесь, вы могли бы сказать, что каждое сообщение записало это имя, когда я использовал его в то время. Я не изменял свое имя до сих пор, поэтому имеет смысл, что более старые сообщения показывают имя, которое я использовал в то время. Это применимо во многих случаях, когда вторичные данные являются транзакционными по своей природе. Разумеется, это не применяется повсеместно, но там, где оно применяется, "не заботясь" - это самый простой подход для всех.
Резюме
В то время как вышесказанное представляет собой просто обширные описания того, как вы могли бы решить эту проблему, и они определенно не полны, я считаю, что каждый раз, когда мне нужно раздувать дубликаты данных, это возвращается к одному из этих основных подходов.