Cloud FirestoreのSnapshot三種や他の型のまとめ

Cloud Firestore のメソッドを使っていて、返ってきた値の扱い方について時々混乱することがありました。
Snapshot の種類と違いをもっと詳細に把握しておけば解決することにふと気づき、調べてみました。

**類似記事**

書き終わってから似た記事があることに気づきました。
まとめ方や重点の置き方に違いがあるので、それぞれに良さがあるのではないかと思います。

- [firestoreのreferenceとかsnapshotで混乱したので整理してみた](https://qiita.com/takahi5/items/a07cc77f6f11c1bd973c)

**コード例などの言語**

TypeScript と Dart/Flutter を使いながら調査しました。
記事中のコードは TypeScript にしていて、説明には Dart のことも含めています。
ウェブ SDK の言語は JavaScript と表現したりもしています。

他の言語で扱うときも大きくは違わないと思います。
クライアント用の SDK で確認したため、Admin SDK では異なるかもしれません。

# 関連する様々な型

Snapshot を細かく見る前に、よく使うプロパティの型やメソッド/ゲッターが返す値の型を全般的に見ておきましょう。

## Referenceとget()

```typescript
db.collection(...)  // CollectionReference
  .get()            // Promise<QuerySnapshot>

db.collection(...)  // CollectionReference
  .doc(...)         // DocumentReference
  .get()            // Promise<DocumentSnapshot>
```

`CollectionReference` と `DocumentReference` は文字通り collection と document を指す参照なのでわかりやすいと思います。

そのどちらにも `get()` があり、基にするのが collection か document かで戻り値が異なります。
それは当然ですが、頭の中であまり区別せずに使ってしまうと混乱の原因になりそうです。

## クエリ条件指定

```typescript
db.collection(...)  // CollectionReference
  .where(...)       // Query
  .order(...)       // Query
  .limit(...)       // Query

db.collectionGroup(...)  // Query
```

先ほど `collection()` の戻り値は `CollectionReference` でしたね。
一方、`collectionGroup()` は `Query` を返します。

`Query` はクエリ条件を指定するメソッド（`where()` など）が返すものです。
`collectionGroup()` も複数の同名の collection をまとめて取得する条件を指定するものであるせいか、`where()` 等に近い扱いになっているようです。[^query]

[^query]: 厳密に言うと `where()`、`limit()` などが返すのは `Query<T>` で、`collectionGroup()` のほうは `Query<DocumentData>` です。ジェネリック型が `DocumentData` に固定されているかどうかの違いなので、それ以上深掘りしないでおきます。

## 複数のdocumentを取得

```typescript
const snapshot = await db.collection(...).get()  // QuerySnapshot

snapshot
  .docs                     // QueryDocumentSnapshot[]
  .forEach(doc => { ... })  // QueryDocumentSnapshot

// JavaScript SDKでは直接forEach()できる
snapshot
  .forEach(doc => { ... })  // QueryDocumentSnapshot
```

`CollectionReference` を使って `get()` すると `QuerySnapshot` が得られます。
その中の `docs` は 配列になっています。
配列の中身の `QueryDocumentSnapshot` は後で詳しく見ます。

なお、JavaScript 用の SDK では `QuerySnapshot` のクラスに `forEach()` メソッドがあるので、`docs` の配列を取り出すことなく直接 `forEach()` で中身を順に取り出せます。
Dart/Flutter の SDK では直接はできません。

## 親を遡る

```typescript
const doc = await db.doc('xxx/xxx').get()  // DocumentSnapshot

const ref1 = doc.ref         // DocumentReference
const ref2 = ref1.parent     // CollectionReference
const ref3 = ref2.parent     // DocumentReference（ないのでnull）
const d = await ref3?.get()  // DocumentSnapshot（ref3がnullなのでgetされずundefined）
```

`QueryDocumentSnapshot` や `DocumentSnapshot` の `ref` で `DocumentReference` を得ることができます。
Dart では `reference` です。

`parent` を使って `CollectionReference`、更にその上の `DocumentReference` と遡っていけます。
ルート collection の参照で `parent` を使うと `null` が返ることに注意しましょう。

`get()` の結果は先ほどと違って `QueryDocumentSnapshot` ではなく `DocumentSnapshot` になります。

## リアルタイムな監視

```typescript
db.collection(...)
  .onSnapshot(snapshot => { ... })  // QuerySnapshot

db.collection(...)
  .doc(...)
  .onSnapshot(snapshot => { ... })  // DocumentSnapshot
```

`onSnapshot`（Dart では `snapshots`）でリアルタイムに更新を監視すると、コールバック関数に `QuerySnapshot` や `DocumentSnapshot` が渡されます。


# 三種類のSnapshot

「Snapshot」が付くものが三つ出てきました。
私が特に混乱していたのはその三つです。

[関連する様々な型](#関連する様々な型) で既に概ね掴めたかもしれませんが、詳細を見ていきます。

※以下、プロパティが Dart ではゲッターになっている場合がありますが、特記は一部を除いて省略します。

## QuerySnapshot

`CollectionReference` で `get()` すると得られるものです。
クエリの結果として得られた `QueryDocumentSnapshot` のオブジェクトが `docs` という配列に入ります。

```typescript:復習
db.collection(...)                  // CollectionReference
  .get()                            // Promise<QuerySnapshot>

db.collection(...)                  // CollectionReference
  .onSnapshot(snapshot => { ... })  // QuerySnapshot
```

`DocumentSnapshot` / `QueryDocumentSnapshot` とは異なり、`get()` や `data()` はありません。
それら二つとは異質なので区別しやすいと思います。

### 主なプロパティ/メソッド

- **docs**
    - クエリの結果として得られた document（`QueryDocumentSnapshot`）のオブジェクトの配列
        - document がなければ空の配列になります。
        - ここから document の情報を取り出して表示等に使うのが主な使い方になるかと思います。
- **size**
    - クエリの結果として得られた document の数
- **metadata**
    - `SnapshotMetadata` という型のデータ
        - バックエンドに保存できていないデータがあるか、キャッシュから取り出したデータか、といった情報を持っています。
- **docChanges**
    - 前回から変化があった document の配列
        - `onSnapshot(snapshot => { ... })` で変更を監視してリアルタイム更新するのに使えます。
        - 最初に取得したときには前回のデータがないので全ての document の情報が入っています。
    - Javascript ではメソッド、Dart ではゲッター

## DocumentSnapshot

`DocumentReference` で `get()` すると得られるものです。
document のデータを持っていて、`data()` で Map として取り出すことができます。[^data]
データがない場合もあります。

[^data]: TypeScript では `data()` の戻り値は `DocumentData` という型ですが、これは文字列をキーとする Map のエイリアスです。

```typescript:復習
const doc = await db
  .doc('xxx/xxx/xxx/xxx')           // DocumentReference
  .get()                            // DocumentSnapshot

const ref = doc.ref.parent.parent   // DocumentReference
await ref?.get()                    // DocumentSnapshot

db.collection(...)                  // CollectionReference
  .doc(...)                         // DocumentReference
  .onSnapshot(snapshot => { ... })  // DocumentSnapshot
```

### 主なプロパティ/メソッド

- **id**
    - document の ID です。
- **metadata**
    - `QuerySnapshot` の `metadata` と同じ
- **exists**
    - document の有無を表す bool 値
- **ref**
    - document の参照
    - Dart では `reference`
- **data()**
    - document のデータを返す
        - データがなければ JavaScript では `undefined`、Dart では `null` が返ります。
- **get()**
    - document のフィールドの値を返す
        - フィールドの名前かパスを引数に取ります。

## QueryDocumentSnapshot

`CollectionReference` で `get()` して得た `QuerySnapshot` の `docs` 内にあるものです。

```typescript:復習
const snapshot = await db.collection(...).get()  // QuerySnapshot

snapshot
  .docs                     // QueryDocumentSnapshot[]
  .forEach(doc => { ... })  // QueryDocumentSnapshot
```

`DocumentSnapshot` を継承していて API の見た目は同じです。

ただし、`exists` に違いが見られます。
「Query」という言葉が付いていることからイメージできるように、クエリの結果として得られたものであり、`QuerySnapshot` の `docs` に入っている時点で確実に存在するので、`exists` は常に `true` になります。

# 参考

## API リファレンス

- ウェブ
    - https://firebase.google.com/docs/reference/js/firebase.firestore
- Flutter
    - https://pub.dev/packages/cloud_firestore