待ち焦がれたCollectionGroupがCloud Firestoreへやってきた。

# CollectionGroupがついにリリース

こんにちは、[Stamp Inc](https://stamp.team/)の[nori](https://twitter.com/1amageek)です。
GWの初日に、ビッグなリリースをぶち込んできやがってなんてことをしてくれんだ。そんなことを思いながら必死でキャッチアップしてまとめました。

## Firebase Cloud FirestoreのCollectionGroup

まだ正式なドキュメントや出てませんが、テストコードの内容から使い方がわかったので参考にして頂ければと思います。

### CollectionGroupとは

まず`CollectionGroup`は何かいつ使うものなのか理解しましょう。
ちょうど昨日Stripeのイベントに登壇したばかりなので、`User`と`Charge`を例に説明します。


`User`は利用者、`Charge`はユーザーの支払い情報を扱うモデルであると考えてください。
<img width="598" alt="スクリーンショット 2019-04-27 15.43.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80287/2d798213-0aad-fda3-ffbd-01a01801d98f.png">

次の要件があるときを考えてみましょう。

__要件__

1. 支払い情報はその支払いを行ったユーザーだけが読み込めるまた書き込める
1. 各ユーザーは支払い金額の合計を算出できる
1. システムでも金額の合計を算出できる

ではこの要件を満足できるシステムをCloudFirestoreで設計しましょう。

#### 今までの使用ではRootCollectionに配置する必要があった

まず考えなければならないのが`システムでも金額の合計を算出できる`です。 Cloud FirestoreではCollectionを飛び越えたQueryをリクエストできないからです。そのためこの要件を満たすためには、`User`と`Charge`はRootCollectionに配置する必要があります。

次に、`各ユーザーは支払い金額の合計を算出できる`を満足させるためにChargeには`userID`を配置する必要があります。またこうすることでどのUserのChargeかを判別可能になるため、セキュリティルールを書くことで`支払い情報はその支払いを行ったユーザーだけが読み込めるまた書き込める`も満たすことが可能です。

```typescript
class Charge extends Doc {
	@Field amount: number = 0
	@Field userID!: string
}

class User extends Doc {
}
```

システム全体の合計を算出するなら次のようになるはずです。

```typescript
const snapshot = await Charge.collectionReference().get()
const total = snapshot.docs
.map(value => value.data()["amount"])
.reduce((prev, current) => prev + current)
```

ユーザーの支払い合計を算出するなら次のようになるはずです。

```typescript
const snapshot = await Charge.collectionReference().where("userID", "==", UESR_ID).get()
const total = snapshot.docs
.map(value => value.data()["amount"])
.reduce((prev, current) => prev + current)
```

#### これからはSubCollectionでもいい

ここで新しく登場したCollectionGroupの機能を見ていきましょう。
CollectionGroupの機能を使うことで、`システムでも金額の合計を算出できる`の要件をRootCollectionではなく、SubCollectionに配置していた場合でも行うことができるようになりました。
<img width="532" alt="スクリーンショット 2019-04-27 16.35.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80287/8b9c23f5-4e3f-12b5-ebcd-fd9a357507d7.png">
ではモデルがどのように変化するか見てみましょう。

```typescript
class Charge extends Doc {
	@Field amount: number = 0
}

class User extends Doc {
	@SubCollection charges: Collection<Charge> = new Collection()
}
```
SubCollectionのPathからセキュリティルールに必要な情報を得ることが可能になったため、`Charge`から`userID`がなくなりました。
具体的な利用例を見ていきましょう。


```typescript
// Chargeを定義
class Charge extends Doc {
	@Field amount: number = 0
}

// Userを定義
class User extends Doc {
	@SubCollection charges: Collection<Charge> = new Collection()
}

const batch: Batch = new Batch()
// User0を保存
{
	const user: User = new User("0")
	const charge: Charge = new Charge()
	charge.amount = 1000
	batch.save([charge], user.charges.collectionReference)
}
// User1を保存
{
	const user: User = new User("1")
	const charge: Charge = new Charge()
	charge.amount = 3000
	batch.save([charge], user.charges.collectionReference)
}
await batch.commit()
// collectionGroupでchargesを取得
const snapshot = await app.firestore().collectionGroup("charges").get()
const total = snapshot.docs
.map(value => value.data()["amount"])
.reduce((prev, current) => prev + current)
```

## CollectionGroup利用の注意点

使ってみて注意が必要だと感じたのは2点です。

1. CollectionGroupは予想以上に横断的
2. DocuementIDが重複する可能性がある
3. Whereが制限される

### CollectionGroupは予想以上に横断的

CollectionGroupのQueryの発行を見てもらうとわかる通り、引数には`collectionId`しか指定できません。
<img width="567" alt="スクリーンショット 2019-04-27 16.46.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80287/a0c73773-16eb-0811-2305-2f7fd410f9ea.png">

全く違うPathで全く違うスキーマであっても`collectionID`さえ一致すれば取得してきます。
<img width="606" alt="スクリーンショット 2019-04-27 16.52.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80287/823de330-def3-95cc-a6fe-4a54c86c6b94.png">

#### `where`を使って取得するPathを制限する

`collectionGroup`はQueryです。いつもの同じように`where`や`limit`などを使うことができます。
取得したいPathを制限するには`where`を利用します。

`where`にFieldPathに`FieldPath.documentId()`を指定しオペレータを設定し制限したいpathを定義します。

```typescript
const user0: User = new User("0")
const user1: User = new User("1")
const snapshot = await app.firestore()
	.collectionGroup("charges")
	.where(FieldPath.documentId(), '>=', user0.documentReference.path)
	.where(FieldPath.documentId(), '<', user1.documentReference.path).get()
```

僕が実験した限りでは`==`オペレーターは動きませんでした。

### DocuementIDが重複する可能性がある

Cloud FirestoreでDocumentを取扱う際、RootCollectionではDocumentIDが重複することはありませんでしたが、CollectionGroupではDocumentを横断的に取得するためDocumentIDが重複する可能性があります。

```typescript
// Chargeを定義
class Charge extends Doc {
	@Field amount: number = 0
	@Field userID!: string
}

// Userを定義
class User extends Doc {
	@SubCollection charges: Collection<Charge> = new Collection()
}

const batch: Batch = new Batch()
// User0を保存
{
	const user: User = new User("0")
	const charge: Charge = new Charge("0")
	charge.amount = 1000
	batch.save([charge], user.charges.collectionReference)
}
// User1を保存
{
	const user: User = new User("1")
	const charge: Charge = new Charge("0")
	charge.amount = 3000
	batch.save([charge], user.charges.collectionReference)
}
await batch.commit()
// collectionGroupでchargesを取得
const snapshot = await app.firestore().collectionGroup("charges").get()
console.log(snapshot.docs.map(snapshot => snapshot.ref.path))
```

```
/user/0/charges/0
/user/1/charges/0
```

今後はdocumentIDでのハンドリングだけでなく`ref.path`でのハンドリングを活用する方がいい場合が出てくるかもしれません。

### Whereが制限される
[CollectionGroupは予想以上に横断的]()の説明でもあるように、取得するCollectionを制限するためにはWhereを使う必要があります。
しかし、Cloud FirestoreのQueryで利用できるWhereには制限があります。複数のフィールドの範囲フィルタは利用することができません。

https://firebase.google.com/docs/firestore/query-data/queries?hl=ja

つまり複数のフィールドの範囲フィルタを必要とする場合は、RootCollectionを利用した方がいいでしょう。

---

## Ballcap

今回は検証のためにBallcapというライブラリを使用しました。BallcapはCloud FirestoreでDocumentのSchemaを定義するためのライブラリです。興味がある方はぜひ利用してみてください。

https://github.com/1amageek/ballcap.ts