firestoreを使うときに理解しておきたいいろんなクラスを整理する

## 概要

firestoreからデータを取得するときに、`CollectionReference`とか`QuerySnapshot`とかいろんなクラスやインターフェースが出てきすぎて悲しみに暮れたのでまとめました。記事中ではjavascriptクライアントを例に取っていますが、他の言語でもそんなに話は変わらないのではないかと思います。

## 登場するクラスやインターフェース

普通にfirestoreを使ってるとよく使うことになるであろう以下のクラスやインターフェースについて役割や使い方を確認していきます。その他のクラスやインターフェースについては [リファレンス](https://firebase.google.com/docs/reference/js/firebase.firestore) を見ると良いでしょう。

- `CollectionReference`
- `Query`
- `DocumentReference`
- `QuerySnapshot`
- `QueryDocumentSnapshot`
- `DocumentSnapshot`

## 説明に使用する例

[公式ドキュメント](https://firebase.google.com/docs/firestore/query-data/get-data) に出てくる都市のデータベースを例として使います。この記事では、以下のクエリで作成される`cities`というコレクションがあるものとして進めます。

```js
var citiesRef = db.collection("cities");

citiesRef.doc("SF").set({
    name: "San Francisco", state: "CA", country: "USA",
    capital: false, population: 860000 });
citiesRef.doc("LA").set({
    name: "Los Angeles", state: "CA", country: "USA",
    capital: false, population: 3900000 });
citiesRef.doc("DC").set({
    name: "Washington, D.C.", state: null, country: "USA",
    capital: true, population: 680000 });
citiesRef.doc("TOK").set({
    name: "Tokyo", state: null, country: "Japan",
    capital: true, population: 9000000 });
citiesRef.doc("BJ").set({
    name: "Beijing", state: null, country: "China",
    capital: true, population: 21500000 });
```

## firestoreからのデータ取得の流れ

具体的な話の前に、firestoreからデータを取得するときに、クライアント側でどういう処理をするかの全体像を確認しておきます。

1. データへの参照を作成する（例えば、`db.collection('cities')`や`db.collection('cities').doc('TOK')`）
2. 参照からスナップショットを得る（`get()`メソッド）
3. スナップショットからデータを得る（`data()`メソッド）

ここで、1の参照の作成と、3のスナップショットからデータへの変換は同期的で、2の参照からスナップショットへの変換だけが非同期（`Promise`が返ってくる）ことを覚えておくと良いでしょう。

それでは、"参照"と"スナップショット"について、具体的に以下で見ていきます。

## 参照（`CollectionReference`、`Query`、`DocumentReference`）

`CollectionReference`、`Query`、`DocumentReference`はいずれもデータへの参照です。このうち、`CollectionReference`と`Query`はほとんど同じもので、いずれも複数のドキュメントへの参照です。`DocumentReference`は一つのドキュメントへの参照です。

#### `CollectionReference`

`CollectionReference`はコレクションやサブコレクションへの参照で、以下のように作成できます。

```js
const citiesRef = db.collection('cities')
```

`CollectionReference`を使って、コレクションにドキュメントを追加することができます。

```js
citiesRef.add(data)
```

また、条件を絞り込んだり、ドキュメントのidを指定することで、後述する`Query`や`DocumentReference`を得ることができます。

```js
const capitalsRef = citiesRef.where('capital', '==', true) // Query型
const SFRef = citiesRef.doc('SF') // DocumentReference型
```

#### `Query`

コレクションの中で、特定の条件を満たすドキュメントだけを取り出したいことや、一定の数のドキュメントを取り出したいことがあります。`Query`は、その絞り込んだ結果への参照で、例えば以下のように生成します。

```js
// capitalプロパティがtrueなドキュメントの集合への参照
const capitalsRef = citiesRef.where('capital', '==', true) 

// ドキュメントの集合３つのみへの参照
const threeCitiesRef = citiesRef.limit(3)

// 人口で昇順にソートされたcitiesへの参照
const citiesSortedByPopulationRef = citiesRef.orderBy('population')
```

`Query`の使い方は、`CollectionReference`とほとんど同じです（ドキュメントによると`CollectionReference`は`Query`のサブクラスのようです）。そのため、例えば`CollectionReference`から絞り込んで`Query`を生成したように、`Query`型をさらに絞り込んで`Query`を生成することができます。以下の例では、`citiesSortedByPopulationRef`という`Query`を絞り込んで、`threeLargestCitiesRef`という別の`Query`を取得しています。

```js
// 人口が多い順にソートしたcitiesへの参照
const citiesSortedByPopulationRef = citiesRef.orderBy('population', 'desc')

// もっとも人口が多い３つの都市への参照
const threeLargestCitiesRef = citiesSortedByPopulationRef.limit(3)
```

#### `DocumentReference`

ある一つのドキュメントの参照が`DocumentReference`です。ドキュメントが所属するコレクションを参照する`CollectionReference`の`doc`メソッドの引数にドキュメントのidを指定することで、以下のように生成できます。

```js
const tokyoRef = citiesRef.doc('TOK')
```

## スナップショット（`QuerySnapshot`、`QueryDocumentSnapshot`、`DocumentSnapshot`）

ここまでは、あくまでデータへの"参照"を取得する話でした。firestoreに保存されているデータの内容を取得するには、"参照"をもとに、実際にデータを持っている"スナップショット"を得る必要があります。"スナップショット"について、以下で説明します。

#### `DocumentSnapshot`

`DocumentSnapshot`は、`DocumentReference`から得られるスナップショットで、単一のドキュメントのデータを持っています。

ここでもう一度、参照からスナップショットを得るための方法について確認しておきます。スナップショットを得るには参照に生えた`get()`メソッドを呼べば良いです。`get()`の返り値の型はスナップショットを返す`Promise`です。例えば、`DocumentReference`から`DocumentSnapshot`を得るには以下のようにします。

```js
const tokyoRef = citiesRef.doc('TOK')
tokyoRef.get().then(docSnapshot => { 
  // docSnapshotはidが'TOK'であるドキュメントのデータをもつDocumentSnapshot
  // ここでdocSnapshotを使ってなんかやる
})
```

もちろん、`async/await`を使っても良いです。

```js
(async () => {
  const tokyoRef = citiesRef.doc('TOK')
  const docSnapshot = await tokyoRef.get()
  // ここでquerySnapshotを使ってなんかやる
})()
```

さて、実際にfirestoreに保存されているデータを取得したいときは、上記の方法で生成した`DocumentSnapshot`の`data()`メソッドを呼びます。

```js
const tokyoRef = citiesRef.doc('TOK')
tokyoRef.get().then(docSnapshot => {
  console.log(docSnapshot.data())
  // { capital: true,
  //   country: 'Japan',
  //   name: 'Tokyo',
  //   population: 9000000,
  //   state: null }
})
```

また、特定のフィールドの値だけが欲しい時は`data()`の代わりに`get(<field名>)`メソッドを呼びます。

```js
const tokyoRef = citiesRef.doc('TOK')
tokyoRef.get().then(docSnapshot => {
  console.log(docSnapshot.get('country'))
  // Japan
})
```

#### `QuerySnapshot`

`QuerySnapshot`は、`CollectionReference`と`Query`から得られるスナップショットです。`DocumentSnapshot`が単一のドキュメントのデータを持っていたのに対して、`QuerySnapshot`は複数のドキュメントのデータを持つスナップショットです。`Query`だけでなく`CollectionReference`からも`QuerySnapshot`が得られることに注意してください。

`QuerySnapshot`は、以下のように使います。`CollectionReference`である`citiesRef`からスナップショットを得ている例です。

```js
citiesRef.get().then(querySnapshot => {
  // ここでquerySnapshotを使ってなんかやる
})
```

`Query`からも全く同じように`QuerySnapshot`が得られます。以下の例では、`querySnapshot`は`capital`が`true`である都市のみのデータを持っています。

```js
const capitalsRef = citiesRef.where('capital', '==', true) // capitalsRefはQuery型
capitalsRef.get().then(querySnapshot => {
  // ここでquerySnapshotを使ってなんかやる
})
```

#### `QueryDocumentSnapshot`

複数のドキュメントのデータを持つ`QuerySnapshot`から実際にデータを取得する際には、一つ一つのドキュメントのデータを持つ`QueryDocumentSnapshot`に対して操作を行います。コレクションから一つ一つドキュメントを取り出して、何らかの操作を行うイメージです。具体的には、主に以下の２つの方法が取られます。

```js
// 1. forEach
citiesRef.get().then(querySnapshot => {
  querySnapshot.forEach(queryDocSnapshot => {
    // ここでqueryDocSnapshotを使ってなんかやる
  })
})

// 2. docs
citiesRef.get().then(querySnapshot => {
  const queryDocsSnapshot = querySnapshot.docs
  // ここでqueryDocsSnapshotを使ってなんかやる
})
```

1の`queryDocSnapshot`は`QueryDocumentSnapshot`、2の`queryDocsSnapshot`は`QueryDocumentSnapshot`の配列になっています。`QueryDocumentSnapshot`はほとんど`DocumentSnapshot`と同じように使えます。例えば、コレクションを構成するドキュメントのデータの配列が欲しいときには、`data()`メソッドを使って以下のようにすれば良いです。

```js
citiesRef.get().then(querySnapshot => {
  const cities = querySnapshot.docs.map(doc => doc.data())
})
```

また、`forEach`を使った例で、`capital`である都市について出力したい場合は、例えば以下のようになるでしょう。

```js
citiesRef.get().then(querySnapshot => {
  querySnapshot.forEach(citySnapshot => {
    const city = citySnapshot.data()
    if (city.capital) {
      console.log(`${city.name} is capital!`)
    }
  })
})
// Beijing is capital!
// Washington, D.C. is capital!
// Tokyo is capital!
```

`QueryDocumentSnapshot`と`DocumentSnapshot`の違いについては明示的に意識しなくても良いと思いますが、例えば [Firestore - Difference between DocumentSnapshot and QueryDocumentSnapshot](https://stackoverflow.com/questions/49859954/firestore-difference-between-documentsnapshot-and-querydocumentsnapshot) を読むとわかると思います。

## まとめ

- firestoreに保存してあるデータを取得する時は、`"参照" --get()--> "スナップショット" --data()--> "データ"`という流れになる
- 以下のクラス/インターフェースがある
    - 参照
        - `CollectionReference`：コレクション（or サブコレクション）への参照
        - `Query`：コレクションから絞り込んだ結果への参照
        - `DocumentReference`：ドキュメントへの参照
    - スナップショット
        - `QuerySnapshot`：`CollectionReference`/`Query`から得られる参照
        - `QueryDocumentSnapshot`：`QuerySnapshot`の構成要素で、単一のドキュメントのデータをもつ
        - `DocumentSnapshot`：`DocumentSnapshot`から得られる参照で、単一のドキュメントのデータをもつ
- [APIリファレンス](https://firebase.google.com/docs/reference/js/firebase.firestore) を読もう