Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

1amageek
I am a geek. Firebase, Firestore 😎 Firebase Japan User Groupのオーガナイザー 半導体エンジニアから、Timers , Cookpadを経て独立。 新規事業の技術顧問として、あらゆる企業をバックアップさせて頂いております。 お困りごとがあれば何なりとご質問ください。
https://stamp.inc/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away