実践Firestoreを読む
こんぶさんのマネをして、読書感想文スクラップを作ってみる
実践Firestore
はじめに
- Firestoreはこれまでのデータベースとは全く異なる機能性を持っている
- 本書では
- 高いスケーラビリティ
- 高いパフォーマンス
- 十分な信頼性
- の3つを備えたアプリケーションの開発を目指す
1. Firestoreの正体
- 最近のモバイル・webアプリ開発では「データベースサーバーサイドロジックの背後にあること」が常識だった
- しかし、Firestoreの「クライアント側から直接アクセスできる」という特徴はそれを覆すものであり、アーキテクチャに大きな変化をもたらした
Firestoreとはなにか
- RealTimeDatabaseの弱点を改善(データモデルの改善・クエリの強化)したドキュメント指向のNoSQLデータベース
NoSQLとは、RDB(データを表みたいに管理する)以外のデータベース全般。
・キーバリュー型:キーとバリューのみのシンプルな組み合わせのモデル
・カラム指向型:キーバリュー型にカラムの概念をもたせたモデル
・ドキュメント指向型:JSONやXML形式で記述されたドキュメントの形で管理するモデル
・グラフ指向型:データとデータ間のつながりを管理するモデル
↑のように様々なデータモデルがある。
Firestoreを最大限活用するためには、従来のようにサーバーサイドAPIを介した呼び出しではなく、クライアントから直接呼び出すことが大事。らしい。
Firestoreがもたらす変化
- データモデリング→アプリとデータの結合度を少なくしよう(クエリやらなんやらにあまり依存しないようにしよう、修正が大変だよ)
- スキーマ変更→アプリの変化とともにデータモデルも変化すべき。これを怠ると後々負債になる
特徴的な機能
-
セキュリテイルール
-
リアルタイムリスナー
-
オフライン対応
-
セキュリテイルール
ユーザーの端末にインストールされて実行されるアプリは危ないから、ちゃんとセキュリテイルール書こうね。
- リアルタイムリスナー
Firestoreの最新の状態をクライアント側に同期するための仕組み。イチからこの機能を作るのはめちゃくちゃ大変やけど、Googleさんが上手くやってくれてはる。
読み取りを最小限にするための仕組みでもある。毎回クライアント側から問い合わせなくてもよいのでread数を抑えられてコスト的にも◎
- オフライン対応
オフライン時に呼ばれた書き込み処理はオペレーション・キューという場所に蓄積され、オンラインが戻ってから再開されるのでよほどのことがない限りエラーにはならない
Cloud Functions
- サーバーサイドのビジネスロジックの実行環境。軽量なロジックを大量に呼び出すことに優れている。
Admin SDK はセキュリテイルールを無視して安全性の高い書き込みができる
- 呼び出し可能な関数(Callableってやつ)
任意のタイミングで呼び出せる
- バックグラウンド関数(onCreateとかのトリガー関数)
イベントをトリガーに呼び出せる。2回以上呼ばれることがあるので冪等性の担保が必須
原則として、Functionsでの読み取り・書き込みは行わない方が良い。したくなったらデータモデルをみなおしてみてはどうでしょうか
ロケーションは物理的に近い
- 東京
- 大阪
にどちらかがおすすめ。
2.データアクセスの基礎
この章では
- データの表現
- データの読み取り、書き込みの操作
について述べられている
Firestoreのデータモデル
まずはFirestoreの各用語の解説
ドキュメント
制約
- ドキュメント1件あたりのサイズが1MiB以下
コレクション
- コレクション
ドキュメントを格納するためのコンテナ。
ドキュメントIDはコレクション内で一意(オンリーワン)であればよく、他のコレクションに同一のIDを持ったドキュメントがあっても大丈夫 - サブコレクション
ドキュメント同士の親子(または所有・非所有)の関係を簡単に表現できる。
時間の経過とともに数が増えていくようなものを入れると良い。 - コレクショングループ
同一のIDを持つコレクションをひとつのコレクションとみなして扱う機能。
リファレンス
リファレンスはFirestoreにデータとして保存することができる。
ドキュメントの関連性を表現する方法として頻繁に使用される。らしい。
スナップショット
ドキュメントやクエリ結果の「取得した瞬間における状態」を表現したデータ。
- fromCache
スナップショット内のドキュメントに、ローカルキャッシュから取得したものが含まれているかどうかを判定するフラグ。(←よくわからない)
- hasPendingWrite
よくわからない。
単一ドキュメントの書き込み
- IDの衝突
Firestoreが自動生成するIDが衝突する可能性はゼロではないが、ほとんどのアプリは衝突が発生するのを見る前に書き込みオペレーションの費用が大変なことになっているはず。笑
createメソッドでIDの衝突が発生した場合、createではなくupdateとして処理されるらしい。
クライアントSDK(アプリから)の書き込みはセキュリテイルールをcreateとupdateで設定できるので、ちゃんと書けば衝突をハンドリングできるが、AdminSDKではルール無視なので、トランザクションの処理を行うときはIDが重複しないか事前に確認する必要がある(めんどくさそう)
updateとset(options.marge = true)ってなにが違うんだ
→ドキュメントが無い場合にupdateだとエラーになるがsetだとエラーにならない
クエリによるデータの取得
- Firestoreのクエリはすべて「強い整合性」をもつんだな
- in句(ORフィルタ)に入れる値は最大10こまで
複合クエリとその制約
- 複数のフィルタを適用したものを複合クエリという
- 演算子系フィルタ(== 、=>など)とin系フィルタ(arrayContains、inなど)を同時に使う場合は「複合インデックス」を登録する必要がある
レイテンシ補正
- listenしているデータへの変更を、クライアントアプリから書き込み(変更)から行う場合、Firebaseへの変更より先にクライアント内のFirebase SDKでデータを最新にすること
- つまり、常に最新のデータをリッスンするための仕組み
トランザクション
- オペレーションがひとつでも失敗すると、すべて自動的にキャンセルされる
一括書き込み(batch)
- 書き込みオペレーション数を減らすことができる
3.オフラインモード
Firestoreのオフラインモードは
- オフラインデータの永続化
- 通信が不安定な状況に体制のあるread、write処理
によって構成されるらしい
オフラインモードの有効化
-
オフラインモードは設定で有効にするかどうかを切り替えることができる
- ネイティブアプリではデフォルトON
- webアプリではデフォルトOFF
-
webでonにすると、タブ間でキャッシュを共有できたりする
-
オフラインモードをONにしている状態では、OFFのときに比べて情報漏洩のリスクがたかまる
4. セキュリティルール
- Firebase上に作られたアプリケーションは、リソースへのアクセスが可能なキーを公開する
- 悪意のある攻撃者は公開されているキーを利用してデータ操作を試みることができる
- その驚異からデータをまもるためにセキュリティルールが必要
安全なセキュリティルールを書くための原則
- セキュリティルールはホワイトリスト形式なので、書くことはセキュリティの穴を開けていくことと同じ
- でもREST APIでも同じなので、恐れる必要ないよ、その役割をセキュリテイルールがするだけ
- 必要最低限の穴を開けられるようにしよう
3つのルール
- 認証認可
- スキーマ検証
- バリデーション
の3つを満たそう
1つのユースケースにひとつのルール
-
特別な事情がない限り、複数のユースケースに適応されるルールは書かないようにしよう(deny時の原因特定に時間がかかる)
-
get ルールはドキュメントに対して評価を行い、list ルールはクエリに対して評価を行う
-
allow write はやめよう(writeにはdeleteが含まれている→deleteはrequest.resourceが設定できない→入ってくるデータのバリデーションがかけない→やめようね)
-
バージョンは最新にしよう(コレクショングループ、つまりすべての階層の同一idコレクションに対するバリデーションが書けるから)
ユーザーに対する認証認可
- カスタムクレームを設定すると、ユーザー属性別(Adminかmemberか)などの認証認可ルールがrequest.authから設定できるようになる
スキーマ検証
開発者が意図しないフィールド形式のドキュメントが生成されることを防ごう
- 一般的な検証パターン
- size(フィールド数)
- keyの名前
- valueの型
をチェックしよう
- バージョンの違いによるフィールドの変化は || で対応しよう
- 正規表現やNGワードなどのバリデーションも書けるよ
セキュリテイルールのテスト
- 他の開発者の邪魔しないようにローカルでテストしようね
- permission denied しか出ないのは安全性のためだよ
5.データモデリング
ドキュメントの設計と、ドキュメント間の関連性の表現方法を解説するぜ
原則
-
フィールドの機密レベルを統一する
- nickNameと住所を同じドキュメント内にいれないようにしようぜ
- 認証認可のルールが書きにくくなるからな
- 公開情報と非公開情報を分けてコレクションにしようぜ
-
バックエンド側での加工ができないので、クライアント側に寄り添った設計にすべき
- データの重複もある程度許そうね
-
ドキュメントは小さくシンプルに
- フィールドが増えれば増えるほどセキュリテイルールを書くのが大変になる
- 参照だけ持たせてクライアントサイドジョインも使おう
- mapやリストはサブコレクション使おうね
- タイムスタンプは目的(ソートや画面表示)を持って持たせようね
-
更新の少ないコレクション設計
- updateのルールは複雑になりがち
1:1リレーション
-
結合(クライアントサイドジョイン)
- idじゃなくてリファレンスを利用しよう
- パスの情報がわからないので、どのコレクションとの関係を持っているかがわかりにくい
- 結局リファレンスに変換されることがほとんど
-
非正規化
- よくクライアントサイドジョインが行われる箇所ではデータを重複させることで、read数を削減する
- リファレンスも持たせることで、元データのonUpdate関数を書く場合にリファレンスによるフィルタリングができる
-
結合VS非正規化
- 非正規化はread数を削減するメリットの一方で、データの更新頻度とのトレードオフ(元データがめっちゃupdateされるようなドキュメントの場合、メリットが消えてしまう)
1:n リレーション
-
サブコレクション
- 上位階層のコレクションがアクセス頻度が多く、深い階層になるにつれてアクセス頻度をさげるような設計
- ルール記述しやすいし機密レベルも統一しやすい
- listやmapは持てるフィールド数に制限がある
- 逆にいうと、数が限られている&&機密レベルも変わらないものの場合、サブコレクションではなくドキュメント内にlistやmapとして持たせる方がいい場合もある
-
コレクショングループ
- 同じID(名前)を持ったコレクションを一括でクエリできる
- コレクション命名の際には、意図しないコレクションとかぶらないように注意しよう
コマンドクエリ責務分離
- readとwriteでオペレーション完全に分けてしまおうよ
- readのルール書くのは簡単だけど、writeのルール変更は大変(特にスキーマ検証とかで)
- だったら「履歴」みたいな小さなコレクション作って、そのonUpdateをトリガーにFunctionsで書き込んじゃえばwriteのルール、履歴コレクションにしか書かなくても良くなるで(天才)
メリット
- read用のモデルに対する変更が他のwrite処理に影響を与えない
- 書き込みの履歴が残る
デメリット
- snapshot.listenで最新の情報が取れない場合が出てくる(書き込みオペレーションによるレイテンシー補正:Firestoreに対する変更の前にクライアント内で変更を反映させる が効かないから。Functionによる反映を待つしかない)ので、「更新後にすぐ最新のものを取得したい」っていうユースケースでは使えない
6. Firestoreでのユーザー管理
レビュー機能のあるショッピングサイトを題材に、これまでの復習
概要
- Authに結びつけたuser情報の管理
- 会員情報の変更履歴を保持
- Twitter連携によるろツイン
- アカウント凍結機能
storeとauthの使い分け
-
Authではユーザー名、プロフィール画像、メールアドレスなどの基本情報に加えて、AdminSDK(Function)を使って好きな値を保存できる領域(カスタムクレーム)に保存することができる
-
場合によっちゃこれだけでユーザー情報管理してstoreでユーザデータ持たなくすることもできるらしい
-
以下のような場合ではstore使おう
- 更新履歴がほしい場合
- ログインユーザー以外がデータを参照する場合
-
以下のような場合ではAuth使おう
- セキュリテイルールからデータ参照したい
- メールアドレスは原則Authへ(まじか)
具体例ばかりなので、その中での気づき
- 一時的な変更listen、Timestampを使ったif文で書くのか、なるほど
- 履歴コレクションを使ったupdate、結局履歴コレクションに対するルールは書かないといけないのがちょっと微妙
7. ショッピングサイトを実装しよう
- 買い物かご
- 商品購入
- レビュー
の3機能を想定して振り返りを行う
買い物かご
気づき
- マルチデバイスでの情報同期はlisterを使う、なるほ
- コレクショングループの登場でルートコレクションの存在意義が減った(ドキュメントIDをオンリーワンにしたい場合にルートコレクションを使う)
- 後半集中力切れたのでまた読む