このガイドでは、Firebase Realtime Database のデータ アーキテクチャの主なコンセプトと JSON データを構築するおすすめの方法を紹介します。
適切な構造のデータベースを構築するには、事前の綿密な計画が必要です。データの保存方法と後で取得する方法をあらかじめ計画し、このプロセスをできる限り簡単にしておくことが最も重要です。
データの構造: JSON ツリー
Firebase Realtime Database のデータは JSON オブジェクトとして保存されます。つまり、このデータベースはクラウドにホストされた JSON ツリーであると考えることができます。SQL データベースとは異なり、テーブルやレコードはありません。データを JSON ツリーに追加すると、既存の JSON 構造内の関連付けられたキーを持つノードになります。ユーザー ID やセマンティック名などの独自のキーを指定することも、push()
によって提供されるキーを使用することもできます。
独自のキーを作成する場合は、UTF-8 でエンコードし、最大 768 バイトにおさめる必要があります。.
、$
、#
、[
、]
、/
、または ASCII 制御文字 0~31、127 を含めることはできません。値そのものに ASCII 制御文字を使用することはできません。
たとえば、基本的なプロフィールと連絡先リストを保存できるチャット アプリケーションについて考えてみましょう。通常のユーザー プロフィールは、/users/$uid
などのパスにあります。ユーザー alovelace
には、次のようなデータベース エントリがあるとします。
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
"contacts": { "ghopper": true },
},
"ghopper": { ... },
"eclarke": { ... }
}
}
データベースでは JSON ツリーが使用されていますが、データベースに保存されているデータを、使用可能な JSON 型に対応する特定のネイティブ型で表現することで、より保守しやすいコードを記述できます。
データ構造のおすすめの方法
データをネストしない
Firebase Realtime Database では最大 32 レベルの深さまでデータをネストできるため、これがデフォルトの構造であると思ってしまうかもしれませんが、そうではありません。データベース内の特定の場所にあるデータをフェッチすると、その子ノードもすべて取り込むことになります。また、データベース内のノードの読み取りアクセス権や書き込みアクセス権をあるユーザーに付与すると、そのノードの下位にあるすべてのデータへのアクセス権もそのユーザーに付与することになります。したがって、実際には、データ構造をできる限りフラットにしておくのが最善です。
ネストされたデータが良くない理由の例として、次のように多重にネストされた構造について考えてみましょう。
{
// This is a poorly nested data architecture, because iterating the children
// of the "chats" node to get a list of conversation titles requires
// potentially downloading hundreds of megabytes of messages
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"messages": {
"m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." },
"m2": { ... },
// a very long list of messages
}
},
"two": { ... }
}
}
このようなネストされた設計でデータを反復処理するのは厄介な作業です。たとえば、チャット会話のタイトルを一覧表示するには、chats
ツリー全体(すべてのメンバーとメッセージも含む)をクライアントにダウンロードしなければいけません。
データ構造を平坦化する
データを複数のパスに分割する(非正規化とも呼ばれます)と、必要に応じて、データを個別の呼び出しで効率的にダウンロードできます。この平坦化された構造について考えてみましょう。
{
// Chats contains only meta info about each conversation
// stored under the chats's unique ID
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
"timestamp": 1459361875666
},
"two": { ... },
"three": { ... }
},
// Conversation members are easily accessible
// and stored by chat conversation ID
"members": {
// we'll talk about indices like this below
"one": {
"ghopper": true,
"alovelace": true,
"eclarke": true
},
"two": { ... },
"three": { ... }
},
// Messages are separate from data we may want to iterate quickly
// but still easily paginated and queried, and organized by chat
// conversation ID
"messages": {
"one": {
"m1": {
"name": "eclarke",
"message": "The relay seems to be malfunctioning.",
"timestamp": 1459361875337
},
"m2": { ... },
"m3": { ... }
},
"two": { ... },
"three": { ... }
}
}
これなら、チャットルームのリストを反復処理するのに会話あたり数バイトのみダウンロードするだけでよく、UI にルーム一覧またはルームを表示するためのメタデータをすばやくフェッチできます。メッセージは個別にフェッチし、到着と同時に表示できるため、UI の応答性と速度が損なわれることはありません。
スケーラブルなデータを作成する
アプリの構築では、リストのサブセットをダウンロードできるようにすると良好な結果が得られることがよくあります。この傾向は、リストに何千件ものレコードが含まれている場合に特によく見られます。この関係が静的かつ単方向の場合は、子オブジェクトを親の下にネストするだけです。
この関係がより動的なときは、データの非正規化が必要になる場合があります。多くの場合は、データの並べ替えとフィルタリングで説明しているように、データのサブセットを取得するクエリを使用して、データを非正規化できます。
しかし、これでもまだ不十分な場合があります。たとえば、ユーザーとグループとの双方向の関係について考えてみましょう。ユーザーはグループに所属でき、グループはユーザーのリストから構成されます。ユーザーの所属先のグループを決定することになると、状況が複雑になります。
必要なのは、ユーザーが所属するグループをリストして、それらのグループのデータだけをフェッチする的確な方法です。この場合、グループのインデックスが大変役に立ちます。
// An index to track Ada's memberships
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
// Index Ada's groups in her profile
"groups": {
// the value here doesn't matter, just that the key exists
"techpioneers": true,
"womentechmakers": true
}
},
...
},
"groups": {
"techpioneers": {
"name": "Historical Tech Pioneers",
"members": {
"alovelace": true,
"ghopper": true,
"eclarke": true
}
},
...
}
}
Ada のレコードの下とグループの下の両方に関係を保存した結果、一部のデータが重複していることに気が付かれたと思います。グループの下に alovelace
のインデックスが作成され、Ada のプロフィールに techpioneers
がリストされています。したがって、Ada をグループから削除するには、2 か所で更新を行う必要があります。
これは、双方向の関係に必要な冗長性です。ユーザーやグループのリストが数百万件の規模にスケーリングされた場合や、Realtime Database セキュリティ ルールで一部のレコードへのアクセスが禁じられている場合でも、この冗長性によって Ada のメンバーシップをすばやく効率的にフェッチできます。
この手法では、ID をキーとしてリストし値を true に設定することで、データを反転させていますが、そうすることで、/users/$uid/groups/$group_id
を読み取ってそれが null
であることを確認するだけで、キーがないかのチェックが済むようにしています。インデックスはデータのクエリやスキャンを実行するよりも迅速かつ効率的です。