1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

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

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

- 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ドキュメントは、それぞれにalbumssongsというサブコレクションが存在します。
そしてそのサブレコレクションの下にもドキュメントが存在するという構造になっています。

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

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

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

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

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

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

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

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

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

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

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

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

パスを使った解決方法

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

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

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-5song-id-6sond-id-7song-id-8songsドキュメントのみが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"で始まるパスを持つすべてのドキュメントを取得することができるようになります。

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と上手く付き合っていく方が良いのかもしれないと最近は思いなが開発をしています。

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

参考にした記事

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?