40

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

そのため、リストノードとリスト インデックスノードを格納し、少なくともリスト名だけは、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 を設定し、解決する前にブラウザーをシャットダウンしました。実際、データベースは一貫性を失い、余分なインデックスが対応する list なしで保存されました。

何かアドバイス?

4

2 に答える 2

73

素晴らしい質問です。私はこれに対する 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、リスナーを呼び出さないことですが、トランザクションも使用しません。

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

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

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

結果整合性

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

概要

上記は、この問題を解決する方法の大まかな説明にすぎず、完全ではありませんが、重複データを展開する必要があるたびに、これらの基本的なアプローチのいずれかに戻ってくることがわかりました。

于 2015-06-07T22:51:57.580 に答える