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を定義するためのライブラリです。興味がある方はぜひ利用してみてください。