「実践Firestore」を読んでの要約・メモ書きです。
良い本だと感じたので、この要約を読んで有益であると感じた場合は購入することをお勧めします。
※注意:発刊が2020年2月なので、情報が古い可能性があります
第1章 Firestoreの正体
- FirestoreをサーバーサイドAPIから読み書きするような構成にしてしまうと、Firestoreのもつメリットが大きく損なわれる
- RDBの世界で忌避される非正規化されたデータ設計も採用されることが多い
- Firestoreでは、複雑なクエリ→「設計の工夫と単純なクエリ」に置き換えることが求められる。アプリケーションとデータベースの結合度をなるべく小さく設計することが重要
- Firestoreをデータベースとして採用する場合は、サーバーサイドのAPIインターフェース変更と同程度にはDBスキーマ変更が発生することを見込まなければいけない。拡張性の高いデータモデルを設計するスキルが重要になる
第2章 データアクセスの基礎
- マップやリストなどのネストした構造は、ユーザーの操作によって時間と共に数が増えていくようなデータを保持する用途に適していない。クエリでの取り回しが悪く、セキュリティルールの観点からも問題になりやすいポイント。この場合は、サブコレクションを使うのが良い。
- Firestoreで大量のデータを読み取る場合は、クエリを分割して必要最小限のデータを繰り返し取得する戦略が、速度・費用の両面で優れる。この場合、Query#limitとクエリカーソルを用いてページネーションを行うことができる。
- 複数ユーザー間や、マルチデバイスで最新のデータを同期したい場合はリアルタイムリスナー機能が有用だ。Firestore上で行われたドキュメントのCRUD処理をサーバー側からクライアントに通知できる。
- Authenticationでユーザーが作られたときにバックグラウンド関数(Cloud function)がトリガされて、ドキュメントが更新されたことを知りたい時(ドキュメント変更を監視したい時)にもリアルタイムリスナー機能は便利。
- 1件以上のドキュメントを同時に更新する際に、整合性を担保する手法として、トランザクション、WriteBatchの2つの方法が提供されている。Write Batch機能の方が、処理速度の面で優れているため、優先的に選択したい(Admin SDK を使う場合などセキュリティルールによる保護が機能しない場合などを除く)
第3章 オフラインモード
- オフラインモードはWEBではデフォルトでOFF、ネイティブではONになっている。
- オフライン時の書き込み処理はキューに保存され、オンライン復帰時に自動的に実行される。したがって、書き込み完了を待ってから次の画面に遷移するような処置はFirebaseで行うべきではない 。原則として、書き込みオペレーションは将来のいずれかの時点で成功するものとして扱って良い。
- 遷移後の画面で書き込んだデータを扱うような場合でも、キャッシュに書き込んだ情報は保存されているので、問題なく読み込むことが可能である。
- トランザクションはオフライン状態では失敗する
- データ読み取りはサーバーを参照し、失敗した場合はキャッシュを参照、という順序で行われる。getメソッドにオプションを指定することでこの順序を変更することができる。リアルタイムリスナーの挙動もこれと同様である。
- 通常とは逆に、キャッシュを優先して取得することも可能である。速度や費用の面でメリットがあるがデメリットもあるため、鮮度が重要でない小規模なデータに短期間で複数アクセスするようなケースでは有効だが慎重に検討すべき。
第4章 セキュリティルール
- セキュリティルールは、ユーザーに対する認証・認可、ドキュメントのスキーマ検証、データのバリデーション、の3つの機能がある。
- セキュリティルールは、ホワイトリスト形式なので、何も書かれていない状態が最も安全であり、追記は壁に穴を開ける行為である。必要最小限の穴を開けるスキルが求められる。
- 1つのユースケースに1つのルール 、を原則として、必要な操作(read,create,update,delete)に満たすべき条件を割り当てていく。ユースケースが1つ増える場合は、ルールも1つ増やす。これにより、保守性が高く安全なルールを設計できる。
- allow write は禁止 。create,update,そしてdeleteも内包しているので危険すぎる
- 認証ロジックは関数として定義することもできる
ユーザーに対する認証・認可
- Authenticationに保持されるユーザーの基本的な情報は管理者しか参照できず、検索や取得のクエリも発行できないため、ほとんどのアプリケーションではこれと1:1対応したFirestoreドキュメントを作るのがおすすめ。この場合は、ドキュメントIDにアカウントIDを割り当てるのが良い。
- セキュリティルール上でもAuthenticationのIDトークン(メールアドレスや認証プロバイダー情報)を利用できるため、これを使ってより高度な認証認可のルールを設計することもできる。これは、匿名でも利用できるが重要な機能はメアドの登録が必要なアプリや、ユーザーの仮登録状態を受け付けるアプリなどで活用できる。
- Admin SDKを使うと、ユーザーのIDトークンに任意の情報(カスタムクレーム)を埋め込むことが可能になる。これによって、ユーザーのロール(管理者/一般)を使って操作を認可したりできる。
ドキュメントのスキーマ検証
- セキュリティルールを認証認可のみで構成すると、認可されたユーザーがどんなデータでも書き込める状態となる(FirestorehaスキーマレスなDB)。これにより、アプリの不具合やデータの不整合が発生することもある。したがって、認証認可と併せて、スキーマ検証も定義しておくべきである。
- スキーマの定義は長くなりやすく、複数回使われることも多いので、関数で定義しておくのがおすすめ。
- スキーマで検証すべきポイントは ①エントリーの数②エントリーの名前③エントリーの型 の3つがある
- arrayやmapのフィールドに対しても①~③の検証をすべきである。要素数が不定であったり、mapのvalueの型が不定である場合は検証ができない。この場合はそもそもarrayやmapではなくサブコレクションを使うべきである。
データのバリデーション
- 認証認可とスキーマ検証を行っても、フィールドに保存されたvalueの妥当性は検証できない。これを検証することも、セキュリティルールの役割である。
- 基本は従来のサーバーサイドアプリケーションに記載されていたバリデーションと同じで、とりうる値の範囲を定義して、それに沿って検証を行うだけ。
-
バリデーションのポイントは以下の4つである
- サイズ上限を設定する。攻撃者によって、意図せぬ巨大なデータが作成されることを防げる
- とりうる値を限定する。正規表現や * in [] も使って、可能な限り限定する。
- リファレンスによる関連チェック。意図したコレクションのドキュメントを参照しているか、また存在しているかを確認する。
- 現在時刻のタイムスタンプ。サーバー側でリクエストを受け取った時刻と書き込もうとしているタイムスタンプが一致するかを確認。
- ドキュメントの更新時もバリデーションをかけるべき 。また、更新して欲しくないフィールドが更新されていないことを確認することも重要。
- セキュリティルールの不備はそのままアプリケーションのバグや脆弱性に繋がってしまう。
- Firebaseにはエミュレーターによってローカル環境でセキュリティルールを展開し、IDトークンの中身を操作しながらテストができる環境が用意されている
第5章 Firestoreデータモデリング
- Firestoreでは、一つのドキュメントに含まれるフィールドの機密レベルを統一することが必要。 各オペレーションや、セキュリティルールの適用ができる範囲が1つのドキュメントであるため。例えば、ニックネームと電話番号は同じドキュメントにおくべきではない。
- 加工なしでそのまま表示できる形でデータモデルを設計するとよい。 取得したデータをアプリ用に加工するコードがクライアント側に書かれる機会を極力減らして、アプリ-Firestore間の結合を減らすことが求められる。
- 1つのドキュメントが大きくなりすぎないようにすべき。 フィールドが増えて取得速度が低下したり、セキュリティルールの適用が難しくなったりする。遅延読み込みがOKなデータはリファレンスだけを持つようにしたり、arrayやmapはサブコレクションを使ったりすべき。
- 更新の少ないコレクション設計をすべき。更新ユースケースのセキュリティルールは複雑になりがちで、バックグラウンド関数での処理に余計な分岐が発生するため。例えば、ユーザーの退会を表現するために、退会フラグをユーザードキュメントに持たせるのではなく、user/unsubscribed-userの2つのコレクションにドキュメントを分ける、などの設計をする。
リレーションの表現
1:1リレーション
- 商品レビューの表示にユーザーデータが必要だとする。ユーザーデータは他の用途でも利用するだろうから、レビューデータとは切り離すのがベターである。
-
レビュードキュメントにユーザードキュメントのリファレンスを持たせるのが最もシンプルなリレーションの表現方法だ。
- ドキュメントの中のリファレンスは、doc.get("XXX.ref")で簡単に取得できる
- ドキュメントIDをリレーションの表現に使用すべきではない。パスの情報が失われているのでどのコレクションのIDなのかがわからなくなる上に、文字列型で表現されるのでIDなのか別の情報なのかが曖昧になる。
- データの非正規化(高頻度で結合が発生する箇所でデータを重複させる)は、読み取り回数を減らして課金額を抑えたり、ドキュメント取得の時間を減らしたりとメリットも多い。
- 上述のレビュードキュメントにユーザーデータを持たせることができる。この場合、ユーザードキュメントの更新があったらレビュードキュメントも更新すべきだ。このようなケースでは、バックグラウンド関数(Firebase Functions)が使える。
- 結合vs非正規化の判断は難しいところだが、まずは結合を試すことをお勧めする。データの鮮度が犠牲になるが、キャッシュ(3章で説明)を利用して結合の課題を解消することもできる。
- 非正規化を選択しても良いケース
- 非正規化によって読み取りオペレーションが劇的に削減できるとき
- 非正規化するフィールドが更新されるユースケースがほぼないとき
- 非正規化を行わなければならないほどの極めて高い性能用件があるとき
- 非正規化を選択してはならないケース
- ドキュメントの機密レベルが異なるとき
- 結合を行わないユースケースがあるとき
- 時間の経過とともに更新される非正規化フィールドが増えていくとき
- 非正規化フィールドの更新頻度が高いとき
- 非正規化を選択しても良いケース
- 同一のIDを割り当てることでリレーションを表現することもできる。 あるドキュメントに対応する別のドキュメントが多くても1個しか存在しないことを保証したいユースケースで有効。
1:nリレーション
- サブコレクションは、1:nを表現する最もシンプルな方法である。 上位のコレクションに格納されているドキュメントの方がアクセス頻度が高い場合が多いので、上位に公開情報、サブコレクションに従属する情報や非公開情報を格納するのが良い。
- arrayやmapでも1:nを表現できる。加工の必要がなく、クエリで取得しやすいドキュメントを作る観点では有効に使える場合もあるが、デメリットも多いため、慎重に活用すべき。
- コレクショングループでも1:nを表現できる。商品レビューを商品ページとユーザーページの両方に表示するようなケースで考えると、商品ドキュメントとレビュードキュメントをサブコレクションで表現して、ユーザードキュメントとレビュードキュメントをサブコレクションで表現するのが良い。これにより、データの重複やドキュメントの肥大化を防ぐことができる。
読み取りに最適なデータモデルを維持しつつ、書き込みの負荷を下げる
- 10や20のフィールドを持つ大きなドキュメントに対して、書き込みの処理を行う場合、セキュリティルールが肥大化し、技術的負債の温床になりがち。これに対して、書き込み用の小さなドキュメントと、更新内容の反映を担当するバックグラウンド関数の2つを用意することで解決することもできる。
- この方法には、書き込み履歴が残せるというメリットがある
- 一方で、リアルタイムリスナーへの反映にラグが出てしまうというデメリットもある。先込みの直後に新しい情報を表示したい、というようなユースケースには向いていない。
第6章 Firestoreでユーザーを管理する
- Authenticationに保存すべきデータ
- セキュリティルールで利用したいデータ
- メールアドレス
- Firestoreに保存すべきデータ
- 変更履歴を管理したいデータ
- アカウント所有者以外が参照するデータ
- Authenticationにはメールアドレスの所有を確認できる機能がある。これにより所有を確認できると、emailVerifiedフラグが立つ。このフラグをセキュリティルールから参照することで、よりセキュアなルールを設計できる。
第7章 FIrestoreでショッピングサイトを実装してみる
- ショッピングサイトの基本機能である「買い物かご」「商品購入」「商品に対するレビュー」の3機能を取り上げて、データモデルの設計や、セキュリティルールの設計、オペレーションコードについて解説されている。