Firestoreを使ったWebアプリケーションを作る際のあれこれ（詰まったとこなど）

# はじめに

WebアプリケーションのバックエンドにFirestoreを採用したので、その際に気をつけるべきこと、詰まったことなどをまとめました。

Firestoreを利用する上で主に参考にしたのは、[公式ドキュメント](https://firebase.google.com/docs/firestore)と[実践Firestore(技術の泉シリーズ)](https://www.amazon.co.jp/dp/B0851BGDQG/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)です。

書籍はFirestoreを利用する上での勘所（どう使うべきか、データモデリング方法やセキュリティルールの書き方など）が書かれており、とても参考になりました。

# データモデリングとセキュリティルールは相互に検討すること

Firestoreとクライアントアプリのやりとりは、間にアプリケーションサーバなどがないため、クライアントから直接データベースを触ることになります。
セキュリティルールを利用して、クライアントからのリクエストを制御することになりますが、**Firestoreのデータモデルの設計がデータのセキュリティに直結**します。

上記の書籍に書かれていますが、まず、**コレクション単位でアクセス可能な権限を統一することが重要**です。

ドキュメントにアクセス可能な権限を持っていれば、そのドキュメント内の全てのフィールドを取得できてしまうため、1つのドキュメントに対して別々のアクセス制御はできません。

例えば、ユーザのプロフィール機能などで、ニックネームは一般に公開したいが、住所などはユーザ本人しか見せたくないといった場合、別のドキュメントとして分離し、別のコレクションに保存する必要があります。

実際にアプリを作る際に、Firestoreのコレクションやドキュメントの設計を行ってから、アプリを実装しましたが、セキュリティルールの都合上実現できないことが出てきて、何度も設計の変更が発生しました。

# リアルタイムリスナーのレイテンシー補正に注意

リアルタイムリスナーを利用すると、Firestore上のドキュメントの変更をクライアントで検知することができるため、常にコレクションの変更を監視したい時に重宝します。

その中で、[レイテンシー補正](https://firebase.google.com/docs/firestore/query-data/listen#events-local-changes)という機能があり、リスナーを登録したコレクションに対して、ローカル（自分自身）で変更があった場合、Firestore（サーバ側）に書き込まれる前にリスナーが検知されます。
サーバに書き込まれた後、再度リスナーに通知が来ますが、データに差分がなければ2回目は無視されます。

ただし、`FieldValue.serverTimestamp()` を利用している場合、サーバ側で書き込んだ際に必ずドキュメントに差分が生まれるため、必ず2回通知が来ます。

サーバへの実際の書き込みを待たずに、すぐに処理を続けることができるため、とても便利ですが、機能の存在を知らないとバグを生み出しかねないので注意が必要です。（私もハマった）

バックエンドに書き込まれていないドキュメントの通知かどうかの判断は、`metadata.hasPendingWrites` を確認することで判別可能です。

```ts
firestore
    .collection(path)
    .onSnapshot({ includeMetadataChanges: true }, (snapShot) => {
        if (snapShot.metadata.hasPendingWrites) {
            // ローカルへの書き込み。バックエンドへの書き込みではない
        }
    })
```

# 複合インデックスのデプロイ方法

コレクションに対して、等価演算子と比較演算子を同時にフィルタとして指定しクエリする場合、複合インデックスが必要になります。

複合インデックスが作成されていないまま、複合クエリを発行すると、エラーが発生します。
このエラーメッセージには、必要な複合インデックスを作成するためのURLが含まれており、クリックすると簡単に作成することができます。
しかし、開発中は重宝するかもしれませんが、別環境(本番環境など)で同じ複合インデックスを作成したい場合、一度エラーメッセージを表示してURLをクリックして、、というのはやってられないので、別の方法で複合インデックスを作成する必要があります。
FirebaseコンソールのGUI操作で作成することもできますが、[CLIで複合インデックスを作成する](https://firebase.google.com/docs/firestore/query-data/indexing?authuser=0#use_the_firebase_cli)ことができます。

`firestore.indexes.json`ファイルを作成し、[フォーマット](https://firebase.google.com/docs/reference/firestore/indexes)を参考に、複合インデックスを定義します。
firebaseコマンドでデプロイすれば、複合インデックスが作成されます。

```
# インデックスのデプロイ
firebase deploy --only firestore:indexes
```

上記では、自分で`firestore.indexes.json`作成する必要がありますが、現在開発環境にデプロイされている複合インデックスを別環境で再利用したい時（もしくはGUIで既に作っていた時）など、デプロイ中の複合インデックスの定義を取得できるコマンドがあります。

```
# 現在の設定値取得
firebase firestore:indexes

# firestore.indexes.jsonとして保存
firebase firestore:indexes > firestore.indexes.json
```

開発中にリンクをタップしてインデックスを作成してから、jsonに書き出してCDに組み込むといった使い方ができます。

# セキュリティルール内でgetメソッドを利用している場合、テストコードが書けない

セキュリティルールの中で、他のコレクションのドキュメントを取得するための`get`というメソッドがあり、
そのセキュリティルールに対してユニットテスト実行時に、そのgetメソッドで取得したドキュメントが存在しない場合、nullで返ってくるわけではなく、権限がないというエラーになってしまいます。

issueに上がっており、エミュレータのバグのようで、まだ解決されていないようです。（放置されぎみ）
https://github.com/firebase/firebase-tools/issues/2067

残念ながら今のところ解決方法がないため、ユニットテストではなく、firebaseコンソール上か、手動でアプリケーションをテストするなどで対応する必要があります。

# まとめ

セキュリティルールのテスト周りはまだベータということもあり、バグがあったり突然の変更があったりしますが、これからもFirestoreをがしがし使っていきたいなと思いました。