Join us for Firebase Summit on November 10, 2021. Tune in to learn how Firebase can help you accelerate app development, release with confidence, and scale with ease. Register

データベースの構造化

このガイドでは、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 であることを確認するだけで、キーがないかのチェックが済むようにしています。インデックスはデータのクエリやスキャンを実行するよりも迅速かつ効率的です。

次のステップ