【Firebase】コレクショングループを使って、横断的に特定のコレクションのサブコレクションのみを取得する方法

# あるサブコレクションが別のサブコレクションにまたがって存在する場合

次のようなFirestoreのコレクションとドキュメントの関係がある場合を考えます。

```markdown
- users (collection)
    - user-id-1 (document)
        - albums (sub collection)
            - alubm-id-1 (document)
                - songs (sub collection)
                    - song-id-1 (document)
                    - song-id-2 (document)
            - album-id-2 (document)
                - songs (sub collection)
                    - song-id-3 (document)
                    - song-id-4 (document)
    - user-id-2 (document)
        - albums (sub collection)
            - album-id-3 (document)
                - songs (sub collection)
                    - song-id-5 (document)
                    - song-id-6 (document)
            - album-id-4 (document)
                - songs (sub collection)
                    - song-id-7 (document)
                    - song-id-8 (document)
```

一番上に`users`コレクションがあり、その下に各`users`ドキュメントが存在します。
その`users`ドキュメントは、それぞれに`albums`と`songs`というサブコレクションが存在します。
そしてそのサブレコレクションの下にもドキュメントが存在するという構造になっています。

このときに、`users/user-id-1`の`songs`のドキュメントのみを取得したい場合どのようにすればよいのでしょうか？

# コレクションとドキュメントを横断的に取得できる「コレクショングループ」という機能

Firestoreにおけるクエリはとても浅いものとなっています。

そのため、ひとつのクエリを使って横断的に複数のコレクションやドキュメントを取得することは原則できません。

しかし、Firestoreではコレクショングループという機能が提供されています。これを使えば横断的にコレクションやドキュメントを取得することができます。

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

使い方は次のようになります。

```kotlin
val query = firestore.collectionGroup("songs").whereEqualTo("category", "jpop")
query.get().await()
```

このクエリーの場合、`songs`という名前のすべてのコレクションに含まれるすべてのドキュメントを取得することができます。

# コレクショングループにおける問題点

実は、このクエリーにはひとつ問題点があります。

それは、あるユーザーが他のユーザーの`songs`サブコレクションの内容まで横断的に取得できてしまうというものです。

例えば、`user-id-2`のユーザーが、自身の`songs`サブコレクションのみを取得しようとして上記のクエリを実行した場合、`user-id-1`の`songs`サブコレクションのドキュメントまで取得できてしまうのです。

# パスを使った解決方法

実は、上記の問題を回避する方法が存在します。それは**パス**と、`orderby()`、`startAt()`、そして`endAt()`を組み合わせて構築するクエリーの方法です。

実際のコードは次のようになります。

```kotlin
val usersRef = firestore.collection("users").document(userId)
val query = firestore.collectionGroup("songs")
    .orderBy(FieldPath.documentId())
    .startAt(usersRef.path)
    .endAt(usersRef.path + "\uf8ff")

val snapshot = query.get().await()
```

このクエリーであれば、`songs`ドキュメントに`"users/$userId"`を表す`DocumentReference`型のフィールドを持たせることなく、`users/$userId`配下に存在する`songs`サブコレクションのドキュメントを横断的に取得することができるようになります。

そして上記のクエリーの結果、`user-id-2`のユーザーが所有する、`song-id-5`、`song-id-6`、`sond-id-7`、`song-id-8`の`songs`ドキュメントのみが`snapshot`として返ってきます。

この時、`orderBy()`を使わないとクエリーが正しく実行されないので注意してください。必ず、最初に`orderBy()`を実行してから`startAt()`と`endAt()`をそれぞれ順番に呼び出してください。


# \uf8ffをエンドガードとして使う理由

Firebaseの公式ドキュメントによると、`\uf8ff`はユニコード上で普段使用される通常の文字の後ろに存在する値のため、クエリで使用すると`startAt()`で指定した値で始まるすべての値に一致するクエリを作成することができるそうです。

例えば、`"/users/user-id-1"`というDocumentReferenceを`startAt()`に渡して、`"/users/user-id-1" + "\uf8ff"`を`endAt()`に渡すことでクエリの両端を制限することができます。

これにより、`"/users-/user-id-1"`で始まるパスを持つすべてのドキュメントを取得することができるようになります。

```kotlin
val usersRef = firestore.collection("users").document(userId)
        val collectionReference = firestore.collectionGroup("albums")
            .orderBy(FieldPath.documentId())
            .startAt(usersRef)
            .endAt(usersRef + "\uf8ff")
```

# まとめ

Firestoreは、通常のRDMSと比べてクエリーの部分が浅く、慣れ親しんだMySQLやPostgreSQLと同じような結果を期待している場合に、思っていた結果と違う内容になってしまい苦労することが多々あると思います。

しかし、近年はコレクショングループのような機能が追加されるなど、昔に比べてサブコレクションを使っても`1:n`の関係を維持しながらドキュメントを取得できるようになってきた気がします。

RDMSと比べて、銀の弾丸と呼ばれるFirestoreのベストプラクティスはまだ確立されていませんが、こういうものだと割り切った上でFirestoreと上手く付き合っていく方が良いのかもしれないと最近は思いなが開発をしています。

もし、上記の方法以外に横断的にサブコレクションを取得するよい方法があれば、コメントをいただけると助かります。

# 参考にした記事

https://medium.com/firebase-developers/how-to-query-collections-in-firestore-under-a-certain-path-6a0d686cebd2

https://note.com/shogoyamada/n/n8fe9486fcbec

https://firebase.google.com/docs/database/rest/retrieve-data?hl=ja