FireStore
NoSQLドキュメント指向データベース
一般的に、アプリケーションで取り扱われるエンティティはオブジェクトモデルによって表現される。
ドキュメント指向データベースではこのオブジェクトモデルをドキュメントという形式で表現し、ストレージに永続化する
FireStore各ドキュメントをコレクションによってグループ化する。
FireStoreのクエリには、高いパフォーマンスを実現する上で犠牲にせざるを得なかったさまざまな制約がある。 データモデルの設計を行う際には、これらの制約をよく理解した上で効率よくデータが取得できるように考慮しなければならない。
オンラインとオフラインの同期
Cloud Firestore のクライアント SDK は、サーバーとの通信が一時的にできない場合でも、ローカルに保持したデータを使ってアプリケーションを動かせます。
ここでいうオフライン対応は、端末同士が直接通信する仕組みではありません。各端末がローカルのキャッシュを読み書きし、ネットワークが利用できるようになったら Cloud Firestore と同期する仕組みです。別の端末で行われた変更は、Cloud Firestore を経由して各端末へ反映されます。
オンライン時の基本的な流れ
アプリがドキュメントやクエリに対してリアルタイムリスナーを登録すると、対象データの変更を検知したときに SDK がスナップショットを渡します。アプリはそのスナップショットを使って画面やメモリ上の状態を更新します。
この「通知」は、OS のプッシュ通知や SMS のような通知ではありません。Firestore への接続を維持しているアプリに対して、SDK が変更イベントを届ける機能です。アプリが終了している端末へ通知を送る用途には、別途 Firebase Cloud Messaging などを検討します。
オフライン時の基本的な流れ
オフラインで書き込んだデータは、端末のローカルにいったん保存されます。書き込みはネットワークが復旧するまで保留され、オンラインに戻ると SDK がサーバーへ送信します。そのため、オフライン中もメモや入力フォームなどの操作を継続できます。
ただし、オフライン中に書き込みが成功したように見えても、Cloud Firestore に到達して処理されたとは限りません。サーバーへの同期結果を確認する必要がある処理では、書き込み完了時のエラーを扱います。
ローカルキャッシュとスナップショット
Firestore の SDK は、取得したデータや書き込み待ちの変更をローカルに保持します。スナップショットには、現在のデータがサーバー由来かキャッシュ由来か、また書き込みがサーバーに確定しているかを判断するためのメタデータがあります。
fromCache: スナップショットがローカルキャッシュのデータを含むかどうかhasPendingWrites: まだサーバーに確定していないローカル書き込みを含むかどうか
たとえば hasPendingWrites が true の間は、画面上で「同期中」と表示できます。fromCache が true の場合は、表示中のデータが最新のサーバーデータではない可能性があります。
オフライン永続化の初期設定は SDK とプラットフォームによって異なります。モバイル SDK では標準で有効な場合がありますが、Web SDK では利用する永続化方式を明示的に設定する必要があります。利用する SDK の仕様を確認し、共有端末ではキャッシュに残るデータの扱いにも注意します。
リアルタイムリスナー
データを一度取得するだけの読み取りとは別に、ドキュメントやクエリにリスナーを登録できます。リスナーは初回に現在の結果を受け取り、その後は対象データに変更があるたびに新しいスナップショットを受け取ります。
リスナー登録
↓
初回データを受信
↓
データ変更を検知
↓
新しいスナップショットを受信
↓
画面やアプリ内状態を更新
リスナーを使うと、一定間隔でデータを取得するポーリングをアプリ側で実装せずに済みます。一方で、画面を離れたあともリスナーを登録したままにすると、不要な読み取りやメモリ使用につながります。不要になったリスナーは解除します。
複数端末で変更した場合
同じドキュメントを複数の端末から変更すると、変更の競合が発生する可能性があります。Firestore は複数端末の変更を自動的に意味のある一つの状態へマージしてくれるわけではありません。単一ドキュメントに対する競合では、基本的に最後に書き込まれた値が優先されます。
たとえば、端末Aと端末Bが同じ name フィールドをオフラインで変更すると、再接続時の書き込み順によって一方の値が失われる可能性があります。次のような設計で競合の影響を小さくします。
- 変更が上書きされても問題ないデータと、履歴を残すデータを分ける
- カウンターにはトランザクションや
FieldValue.increment()を使う - 複数のフィールドを条件付きで更新する場合はトランザクションを使う
- 編集者、編集時刻、バージョンなどを保存して競合を検出する
- 同じドキュメントを複数人が編集する場合は、アプリ側でマージや競合解決のルールを決める
リアルタイム同期は通信を自動化しますが、データの意味を理解した競合解決まで自動化する機能ではありません。
同期のライフサイクル
アプリでデータを編集してから、他の端末に変更が表示されるまでには次の段階があります。
- ユーザーの操作を受けて、端末のローカル状態を更新する
- SDK が書き込みをローカルキャッシュと保留中の書き込みに反映する
- ネットワークが利用できれば、Cloud Firestore に書き込みを送信する
- Cloud Firestore が認証、セキュリティルール、データ整合性を確認する
- サーバーで確定した変更が、接続中の各端末のリスナーへ届く
- 各端末が受け取ったスナップショットを画面やローカル状態へ反映する
この流れを理解しておくと、「入力直後には表示されたが、あとで書き込みエラーになった」「オフライン中は見えていたデータが再接続後に変わった」といった挙動を説明しやすくなります。
設計上の注意点
- リアルタイムリスナーの登録と解除を画面のライフサイクルに合わせる
- オフライン中の表示を最新データと誤認しないようにする
- 同期前のデータを表示する場合は、保留中であることを必要に応じて示す
- 書き込みエラー、認証切れ、セキュリティルールによる拒否を処理する
- 競合時に上書きされてもよいフィールドかどうかを決める
- オフラインキャッシュに残してよいデータか、端末共有時に問題がないかを確認する
- リスナーやクエリの利用量を確認し、不要な読み取りを減らす
Firestore のオフライン対応とリアルタイムリスナーは、共同編集、チャット、タスク管理、在庫表示などに向いています。一方で、強い整合性が必要な処理や複雑な共同編集では、トランザクション、サーバー側処理、競合解決の設計を組み合わせる必要があります。
FireStore Tips
これだけは覚えておけ
FireStoreにおけるデータ構造の表現であるドキュメントとコレクション。
データアクセスの基礎となるリファレンスとスナップショット
FireStoreのドキュメントにはデータサイズの上限がある。
ドキュメントの型である、マップやリストなどのネストした構造はアプリケーションによって生成される(つまり、ユーザの操作によって時間とともに数が増えていくような)適していない。そのためのサブコレクション
書き込みに関しては単一のドキュメントに対する操作だけが定義されている。
→RDBのように1回のクエリで複数レコードの特定の列をまとめて更新するといった機能が提供されていない。このような操作は対象となるドキュメントをすべて取得してからひとつひとつ更新をかける形で実現する。
ドキュメント
ドキュメントはJSONによく似た構造化されたデータデータ。 アプリケーションが扱うモデルを構成する関連のある一連のデータ ドキュメントはJSONで利用可能な基本的な型に加え、タイムスタンプ型や経緯緯度といったアプリケーションで取り扱う頻度の高いデータ型を他の型に変換することなく保存したり取り出したりできる。 これによりORMの手間を大幅に省略できる。
コレクション
コレクションはドキュメントを格納するためのコンテナー FireStoreはスキーマレスのため同一のコレクション内であっても各ドキュメントにどのようなフィールドをもたせるかを自由に設計できるが、ほとんどのユースケースにおいてはコレクション内のすべてのドキュメントが同一のフィールドを持つようにすべき。 →このような設計方針を採用することで、クエリの発行やセキュリティルールでの取り扱いが容易になる(まあ当たり前か)
制約 コレクション内の各ドキュメントにはそれぞれ一意なIDを割り当てる必要がある。 IDはコレクション内で一意であればよく、他のコレクションに同一のIDを持つドキュメントが存在しても問題ない。
コレクショングループ
同一のIDを持つコレクションをひとつのコレクションとみなして扱う機能。 ※通常のクエリでは単一のコレクションからしかデータを取得できない。
スナップショット
スナップショットとは、ドキュメントやクエリ結果のある瞬間における状態を表現したデータ 通常はドキュメントを取得した瞬間やクエリによるデータ取得が完了した瞬間にスナップショットが作成される。
リファレンス
ドキュメントやコレクションが格納されているFirestore内のパスを表現するモデル ※リファレンスはそのままFirestoreにデータとして保存できる。
fromCache
スナップショットへ含まれるドキュメントに、ローカルキャッシュから取得したものが含まれているかどうかを判定するフラグ キ