thumbnail

※本ブログの目的と内容1、著作者の方へ2

Firebaseの概要

BaaSとしてのFirebase

スモールスタートし改善を繰り返すようなWebサービスがFirebaseの使いどころとしてもっとも適している。

Firebaseのメリット
  • 速やかに開発に着手できる
  • リアルタイム機能やスケーラビリティを手軽に享受できる
Firebaseのデメリット
  • Googleにベンダーロックインされる
  • Firestoreで複雑なデータ構造を取り扱う際にはデータモデルに注意が必要
  • 大規模な開発にはあまり向かない

マルチテナントとは

複数の顧客で1つのアプリケーションやデータベースのインスタンスを共有して使う方式のこと。

設計編

Firestoreを使うときは早めに認証や認可の要件を固めてデータモデルを整理する。

Firebaseのサービス構成

Cloud Firestore

セキュリティルール

明示的に書かれた権限のみ許可される。

データ整合性
  • トランザクション
    読み込みと書き込みが複数回行われる際に、整合性のとれた状態で一連の操作ができるように保証する
  • バッチ書き込み
    複数回の書き込みのみを行いたい場合に、一連の更新処理を一貫性を保ったまま実行できる
課金体系

ドキュメント単位の読み取りおよび書き込みに対して課金される。

Firebase Hosting

プレビューチャネル(一時的なURLを発行する)は、あくまで公開先のURLと配信リソースを複数持てるというだけで、FirestoreやStorageなどは同じものを共有する。

Firebaseの概要設計

要件を整理する

概念データモデルを作成する

概念データモデルはシステムで必要となる実体(エンティティ)を抽出し、エンティティ間の関係(リレーション)を整理すること。

conceptual-data-model

Firestoreのデータモデルを物理設計する

データモデリング

RDB

厳密な整合性の保証を重視。 「テーブルの正規化」によってモデリングする。

Firestore(NoSQL)

RDBよりもスケーラビリティを優先。 厳密な整合性は、スケーラビリティとのトレードオフの関係にある。 製品ごとに特性の違いも大きく、決まったモデリングの手法がない。

正規化

RDB
  • 正規化
    データの冗長性がなくなるように適切にテーブルを分解することを意味する
Firestore
  • 正規化
    1つのデータを1箇所にしか保存しないことを意味する
  • 非正規化
    1つのデータを異なる複数の場所にコピーして保存することを意味する
    スナップショットデータを保持するようなケースでは積極的に非正規化すべき

結合

RDB

クエリ処理(結合等)はデータベース内で完結する。

rdb-join

Firebase

コレクション毎にクエリを行い、クライアント側でデータを結合する。クライアントジョインと呼ばれる。 クエリ回数が件数に比例して膨らむ。件数増加とともに、リソース逼迫によるUX低下が懸念される。

firestore-join

非正規化とデータ整合性

クライアントサイドジョインを避けるために、意図的な非正規化が有効。 ただし、データ重複部分に整合性の問題が生じる。 整合性を担保するために、元ドキュメントの更新時に、非正規化ドキュメントにも更新を反映させる必要がある。

  1. トランザクション機能でアトミックに複数ドキュメントを更新する
  2. ドキュメント更新時トリガーで更新する
ドキュメント更新時トリガー
  • メリット
    • 同期処理を開発者が意識しなくてもよくなり、実装漏れをなくせる
  • デメリット
    • トリガーを多用すると処理のつながりが見えにくくなる
    • 同時にドキュメントの更新がかかり、古いデータに同期されるデータ不整合リスク

Firestoreで関係を表現するための機能

サブコレクション

一体多の依存関係にあるエンティティで有効。 結合(クライアントサイドジョイン)ではなく、サブコレクションをうまく利用するとFirestoreの特性を活かしやすくなる。

サブコレクションではなく、ドキュメント内の配列を利用することには以下のデメリットがある。

  • 配列内に対するセキュリティルール(バリデーション)を書けない
  • サイズ増大時、ドキュメント取得のオーバーヘッドが増加する
  • サイズ増大時、Firestoreのドキュメントサイズ上限に抵触する

データモデリングパターン 一対一

RDBでは、主キーが同一の一対一のエンティティ同士は、1つのテーブルにまとめるのが一般的。

one-to-one

共通IDのメリット
  • 一対一という関係を保証できる
    フィールドに参照型として持つと、一対多となる可能性を秘める
  • 異なるアクセス制御を定義できる
    一部のフィールドだけ非公開にするような制御は行えない

データモデリングパターン 一対多

サブコレクションを利用する代表的なパターン。

one-to-many

データモデリングパターン 多対多

RDBでは、中間テーブルを切り出して両テーブルの主キーのマッピングを保持することが一般的。

many-to-many

認証を設計する

Firestoreのセキュリティ機能はコレクションやドキュメントのデータ構造を利用して実装する。

認可を設計する

認可設計の検討ポイント

Firestoreの権限制御は、セキュリティルールによって実現する。 カスタムクレームに権限情報を保持し、ユーザ単位でのアクセス制御を行う。 フロントサイドからコールされたfunctionsで処理を行う際は、まず認可の制御を行う。

テナントベースのアクセス制御

テナントデータのコレクション構造

特定テナントに依存しない横断的データは、トップレベルコレクションに配置する。

カスタムクレームを利用したテナント所属チェック

テナントコレクション以下のデータにアクセスする際、パスのテナントIDカスタムクレームのテナントIDを比較してアクセスを許可する。

テナント所属ユーザの管理

ユーザ登録されたタイミングで、そのユーザのカスタムクレームにテナントIDを登録する。 ログインのタイミングで、カスタムクレームのテナントIDの権限を依然として保持しているか確認するとより安全。

ロールベースのアクセス制御

ロールはテナント単位で付与されるため、カスタムクレームで管理する場合はやや構造が複雑になる。

ロールが持つ権限でアクセス可否を判断する関数
function isApplicationUser(tenantId) {
  return get(/databases/$(database)/documents/tenantUsers/$(request.auth.uid)/tenants/$(tenantId)).data.role == ['applicationUser', 'applicationManager'];
}

個人ベースのアクセス制御

ユーザと一対一の関係にあるエンティティの場合、ドキュメントIDにUIDを利用する。

ユーザ自分自身のデータであれば読み書きできるセキュリティルール
function isOwnData(docId) {
  return request.auth.uid == docId;
}
ユーザ単位で読み書きを制御するセキュリティルール

アクセス先のドキュメントデータ(resource.dataオブジェクト)を参照することで権限判定できる。

ドキュメント

{
  fileName: "メンバーリスト.pdf",
  viewers: ["user1", "user2", "user3"]
}

セキュリティルール

match /files/{fileId} {
  allow read: request.auth.id in resource.data.viewers;
}

ユーザアカウントの登録時の処理

ユーザ作成はFirebase Admin SDKを用いてバックエンドで実行する。

セキュリティルールまとめ

権限管理上は必要最低限の権限のみ付与すべき。

  • read(読み取り)
    • get(単一ドキュメントの取得)とlist(コレクションからの複数取得)に分けられる
  • write(書き込み)
    • create(新規作成)、update(更新)、delete(削除)に分けられる

コレクションのデータモデリングを完成させる

予期せぬデータの混入はバグの発見を遅らせ、より致命的な障害に発展する可能性がある。可能な限りバリデーションを組み込む。

data-model

全ドキュメント共通のシステムフィールド

_は、「バックエンドで管理する」という意味が込められている。

フィールド名 理論名 値(例)
_updatedAt 最終更新日時 日時 2024-04-23T00:00:00Z
_updatedBy 最終更新者 文字列 user1

タイムスタンプ値には、サーバサイドの信頼ある値のみ受け入れるようにセキュリティルールでガードする。

function isValidCreated() {
  return
    request.resource.data._updatedAt == request.time &&
    request.resource.data._updatedBy == request.auth.uid;
}

開発編

複数環境を管理する

WebサービスとFirebaseプロジェクトの紐づけは、initializeAppに指定するfirebaseConfigの値で決まる。 firebaseConfigの値をenvファイルに切り出し、dotenvなどで切り替えることで、Webサービが接続するプロジェクトを変更できる。

Firebase SDK v9のリリース

v9ではモジュール形式が採用され、必要な機能だけを読み込めるようになった。 ツリーシェイキングが容易になり、バンドルサイズが大幅に削減できる。

Cloud Storage

認可を想定したStorageのオブジェクト階層構造

FirestoreとStorageのデータ階層構造は極力一致させる。

ダウンロードのためのCORS設定

gsutil(Cloud Storage用CLI)でCORSを設定する。

  1. 本ブログは「本を読み、理解した内容の備忘録(自分用)」を目的としている。重要なアイディアを昇華させ、自分の言葉でまとめるように努めている 

  2. 内容に不快を感じ、ブログの取り下げを希望される著作者の方は、個別にご連絡いただけると幸いに思う