CollectionGroupがついにリリース
こんにちは、Stamp Incのnoriです。
GWの初日に、ビッグなリリースをぶち込んできやがってなんてことをしてくれんだ。そんなことを思いながら必死でキャッチアップしてまとめました。
Firebase Cloud FirestoreのCollectionGroup
まだ正式なドキュメントや出てませんが、テストコードの内容から使い方がわかったので参考にして頂ければと思います。
CollectionGroupとは
まずCollectionGroupは何かいつ使うものなのか理解しましょう。
ちょうど昨日Stripeのイベントに登壇したばかりなので、UserとChargeを例に説明します。
Userは利用者、Chargeはユーザーの支払い情報を扱うモデルであると考えてください。

次の要件があるときを考えてみましょう。
要件
- 支払い情報はその支払いを行ったユーザーだけが読み込めるまた書き込める
- 各ユーザーは支払い金額の合計を算出できる
- システムでも金額の合計を算出できる
ではこの要件を満足できるシステムをCloudFirestoreで設計しましょう。
今までの使用ではRootCollectionに配置する必要があった
まず考えなければならないのがシステムでも金額の合計を算出できるです。 Cloud FirestoreではCollectionを飛び越えたQueryをリクエストできないからです。そのためこの要件を満たすためには、UserとChargeはRootCollectionに配置する必要があります。
次に、各ユーザーは支払い金額の合計を算出できるを満足させるためにChargeにはuserIDを配置する必要があります。またこうすることでどのUserのChargeかを判別可能になるため、セキュリティルールを書くことで支払い情報はその支払いを行ったユーザーだけが読み込めるまた書き込めるも満たすことが可能です。
class Charge extends Doc {
@Field amount: number = 0
@Field userID!: string
}
class User extends Doc {
}
システム全体の合計を算出するなら次のようになるはずです。
const snapshot = await Charge.collectionReference().get()
const total = snapshot.docs
.map(value => value.data()["amount"])
.reduce((prev, current) => prev + current)
ユーザーの支払い合計を算出するなら次のようになるはずです。
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に配置していた場合でも行うことができるようになりました。

ではモデルがどのように変化するか見てみましょう。
class Charge extends Doc {
@Field amount: number = 0
}
class User extends Doc {
@SubCollection charges: Collection<Charge> = new Collection()
}
SubCollectionのPathからセキュリティルールに必要な情報を得ることが可能になったため、ChargeからuserIDがなくなりました。
具体的な利用例を見ていきましょう。
// 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点です。
- CollectionGroupは予想以上に横断的
- DocuementIDが重複する可能性がある
- Whereが制限される
CollectionGroupは予想以上に横断的
CollectionGroupのQueryの発行を見てもらうとわかる通り、引数にはcollectionIdしか指定できません。

全く違うPathで全く違うスキーマであってもcollectionIDさえ一致すれば取得してきます。

whereを使って取得するPathを制限する
collectionGroupはQueryです。いつもの同じようにwhereやlimitなどを使うことができます。
取得したいPathを制限するにはwhereを利用します。
whereにFieldPathにFieldPath.documentId()を指定しオペレータを設定し制限したいpathを定義します。
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が重複する可能性があります。
// 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には制限があります。複数のフィールドの範囲フィルタは利用することができません。
つまり複数のフィールドの範囲フィルタを必要とする場合は、RootCollectionを利用した方がいいでしょう。
Ballcap
今回は検証のためにBallcapというライブラリを使用しました。BallcapはCloud FirestoreでDocumentのSchemaを定義するためのライブラリです。興味がある方はぜひ利用してみてください。