Edited at

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


CollectionGroupがついにリリース

こんにちは、Stamp Incnoriです。

GWの初日に、ビッグなリリースをぶち込んできやがってなんてことをしてくれんだ。そんなことを思いながら必死でキャッチアップしてまとめました。


Firebase Cloud FirestoreのCollectionGroup

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


CollectionGroupとは

まずCollectionGroupは何かいつ使うものなのか理解しましょう。

ちょうど昨日Stripeのイベントに登壇したばかりなので、UserChargeを例に説明します。

Userは利用者、Chargeはユーザーの支払い情報を扱うモデルであると考えてください。

スクリーンショット 2019-04-27 15.43.25.png

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

要件


  1. 支払い情報はその支払いを行ったユーザーだけが読み込めるまた書き込める

  2. 各ユーザーは支払い金額の合計を算出できる

  3. システムでも金額の合計を算出できる

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


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

まず考えなければならないのがシステムでも金額の合計を算出できるです。 Cloud FirestoreではCollectionを飛び越えたQueryをリクエストできないからです。そのためこの要件を満たすためには、UserChargeは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に配置していた場合でも行うことができるようになりました。

スクリーンショット 2019-04-27 16.35.54.png

ではモデルがどのように変化するか見てみましょう。

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点です。


  1. CollectionGroupは予想以上に横断的

  2. DocuementIDが一致する

  3. Whereが制限される


CollectionGroupは予想以上に横断的

CollectionGroupのQueryの発行を見てもらうとわかる通り、引数にはcollectionIdしか指定できません。

スクリーンショット 2019-04-27 16.46.52.png

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

スクリーンショット 2019-04-27 16.52.21.png


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

collectionGroupはQueryです。いつもの同じようにwherelimitなどを使うことができます。

取得したい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には制限があります。複数のフィールドの範囲フィルタは利用することができません。

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