素晴らしい質問です。私はこれに対する 3 つのアプローチを知っています。以下にリストします。
これについては、少し異なる例を取り上げます。これは主に、説明でより具体的な用語を使用できるためです。
メッセージとユーザーという 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
(so:209103 と so: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's data: ", snapshot.val());
}, false /* don't apply the change locally */);
かなり関与しており、抜け目のない読者は、私がメッセージの処理をごまかしていることに気付くでしょう。最初のチートはoff
、リスナーを呼び出さないことですが、トランザクションも使用しません。
このタイプの操作をクライアントから安全に実行したい場合は、次のものが必要です。
- 両方の場所の名前が一致することを保証するセキュリティ ルール。ただし、ルールは、名前を変更している間、一時的に変更できるように十分な柔軟性を持たせる必要があります。したがって、これは非常に苦痛な 2 フェーズ コミット スキームになります。
- メッセージのすべての
username
フィールドをso:209103
to null
(魔法の値)で変更します
name
ユーザーso:209103
の を「puf」に変更します
username
すべてのメッセージのso:209103
をに変更null
しますpuf
。
- そのクエリには、
and
Firebase クエリがサポートしていない 2 つの条件のいずれかが必要です。そのため、クエリを実行できる追加のプロパティuid_plus_name
( value を持つ) ができあがります。so:209103_puf
- これらすべての遷移をトランザクション的に処理するクライアント側コード。
この種のアプローチは頭が痛くなります。そして通常、それは私が何か間違ったことをしていることを意味します. しかし、それが正しいアプローチであったとしても、頭が痛いほど、コーディングの間違いを犯す可能性が高くなります。したがって、私はより簡単な解決策を探すことを好みます。
結果整合性
更新 (20150925) : Firebase は、複数のパスへのアトミック書き込みを許可する機能をリリースしました。これは以下のアプローチと同様に機能しますが、単一のコマンドを使用します。これがどのように機能するかについては、上記の更新されたセクションを参照してください。
2 番目のアプローチは、ユーザー アクション (「名前を 'puf' に変更したい」) をそのアクションの意味 (「プロファイル so: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 で最適なパフォーマンスを得るには悪い考えです)。しかし、すべてのデータが同時に完全に更新されるわけではありませんが、全体としてアプローチはより単純です。しかし最終的には、新しい値に一致するようにすべてのメッセージが更新されます。
構わない
3 番目のアプローチは最も単純です。多くの場合、複製されたデータを更新する必要はまったくありません。ここで使用した例では、各メッセージには当時私が使用していた名前が記録されていたと言えます。私は今まで名前を変えていなかったので、古いメッセージには当時使っていた名前が表示されているのは理にかなっています。これは、二次データが本質的にトランザクションである多くの場合に当てはまります。もちろん、どこにでも当てはまるわけではありませんが、「気にしない」というのが最も単純なアプローチです。
概要
上記は、この問題を解決する方法の大まかな説明にすぎず、完全ではありませんが、重複データを展開する必要があるたびに、これらの基本的なアプローチのいずれかに戻ってくることがわかりました。