web-dev-qa-db-ja.com

Firebaseで非正規化データを書き込む方法

Stucturing Data に関するFirebaseのドキュメントを読みました。データストレージは安価ですが、ユーザーの時間はそうではありません。 get操作を最適化し、複数の場所に書き込む必要があります。

したがって、listノードとlist-indexノードを格納する可能性があります、少なくともリスト名で、2つの間にいくつかの重複データがあります。

私はES6を使用しており、JavaScriptアプリで非同期フローを処理することを約束しています。主に、最初のデータプッシュ後にFirebaseからrefキーをフェッチします。

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);
 });

アプリがクライアントでのみ実行されることを確認しながら、データがすべての場所で同期されていることを確認するにはどうすればよいですか?

健全性チェックのために、promiseにsetTimeoutを設定し、解決する前にブラウザーをシャットダウンしました。実際、データベースの整合性が失われ、対応するリストなしで追加のインデックスが保存されました

何かアドバイス?

35
collardeau

素晴らしい質問です。私はこれに対する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など:3648524)を格納します。ただし、alsoはメッセージにユーザーの名前を格納するため、リストを表示するときにユーザーごとにこれを検索する必要はありません。メッセージの。

では、チャットサービスのプロフィールページに移動して、名前を「FrankvanPuffelen」から「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を呼び出さないことですが、トランザクションも使用しません。

このタイプの操作をクライアントから安全に実行するには、次のものが必要です。

  1. 両方の場所の名前が一致することを保証するセキュリティルール。ただし、ルールでは、名前を変更している間、一時的に異なるように十分な柔軟性を持たせる必要があります。したがって、これはかなり苦痛な2フェーズコミットスキームになります。
    1. _so:209103_によるメッセージのすべてのusernameフィールドをnull(魔法の値)に変更します
    2. ユーザー_so:209103_のnameを「puf」に変更します
    3. すべてのメッセージのusernameを_so:209103_、つまりnullからpufに変更します。
    4. そのクエリには2つの条件のandが必要ですが、Firebaseクエリではサポートされていません。したがって、クエリを実行できる追加のプロパティ_uid_plus_name_(値は_so:209103_puf_)になります。
  2. これらすべての遷移をトランザクションで処理するクライアント側のコード。

このタイプのアプローチは私の頭を傷つけます。そして通常、それは私が何か間違ったことをしていることを意味します。しかし、それが正しいアプローチであったとしても、頭が痛いので、コーディングの間違いを犯す可能性がはるかに高くなります。だから私はもっと簡単な解決策を探すことを好みます。

結果整合性

更新(20150925):Firebaseは、複数のパスへのアトミック書き込みを許可する機能をリリースしました。これは以下のアプローチと同様に機能しますが、コマンドは1つです。これがどのように機能するかを読むには、上記の更新されたセクションを参照してください。

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番目のアプローチは、すべての中で最も単純です。多くの場合、複製されたデータを実際に更新する必要はまったくありません。ここで使用した例では、各メッセージに、その時点で使用した名前が記録されていると言えます。私は今まで名前を変更していなかったので、古いメッセージにそのときに使用した名前が表示されているのは理にかなっています。これは、二次データが本質的にトランザクションである多くの場合に当てはまります。もちろんどこにでも当てはまるわけではありませんが、「気にしない」というのが最も簡単なアプローチです。

概要

上記はこの問題を解決する方法の大まかな説明であり、完全ではありませんが、重複データをファンアウトする必要があるたびに、これらの基本的なアプローチの1つに戻ることがわかります。

61

Franksのすばらしい回答に追加するために、一連の Firebase Cloud Functions を使用して結果整合性アプローチを実装しました。関数は、プライマリ値(ユーザー名など)が変更されるたびにトリガーされ、非正規化されたフィールドに変更を伝播します。

トランザクションほど高速ではありませんが、多くの場合、高速である必要はありません。

6
Uffe Koch