SubCollectionいつ使うの問題
Cloud Firestoreがリリースされて数日経ちました。SaladaをFirestoreに対応させるため、仕様の深いところまで検証しているところです。そんな中でぶち当たった問題について記載します。
TL;DR
- Queryに依存するアプリを作ると拡張性を失う。
- QueryはElasticSearchに任せる。
- SubCollectionの使い所は限定的。
SubCollectionとは
FirestoreはCollection
とDocument
とData
で構成されます。
Collectionは複数のDocumentを持つことができ、
DocumentはDataとCollectionを持つことができます。Documentが持つCollectionのことをSubCollectionと言います。
なぜSubCollectionが必要だったのか
Firebase Realtime Databaseでは深いネストの上位ノードでデータを取得すると、そのノード以下全てを取得してしまう課題がありました。
例えばv1
でデータを取得するとfollower
もuser
も全部取得しちゃう。通信がえげつないことになる訳です。
そこで登場したのがFirestoreのSubCollection
の考え方。DocumentをDataとCollectionに分ける事でDataだけを取得するようになりました。めでたしめでたし。
ところが、機能が拡張された事でFirebaseの考え方が少し変化しました。
__WHERE__がなくとも
Firebase Realtime Databaseには柔軟なQueryがありませんでしたがFirestoreではwhereを利用できるようになりました。しかしWhereがなくとも多彩な機能を表現できます。
ここでフォロー機能について考えて見ましょう。
Firebaseでフォロー機能を実現するにはおおよそ3つの方法があります。勝手に名前をつけましたが、ちゃんとした名前があったら誰か教えてください。
- 男は黙って冗長型
- ネスト参照型
- リレーションシップ参照型
男は黙って冗長型
これは、Firebase Castでも紹介されている。強力な方法です。
user_0
がuser_1
をフォローしている状態を表すためにfollowers
にuser_1
ごと入れ込んじゃう。
メリット | デメリット |
---|---|
読出し高速 | データ量増加 複数のノードに同時に書き込む |
user_0
を取得したタイミングでuser_1
の情報も取得することになるのでもちろん読出しは高速になります。しかしこれも最初のうちでデータが増加するに連れてuser_0
は肥大化していきます。そして複数のノードに対して変更を加えないといけない未来が待っています。
例えばuser_0
,user_1
,user_2
,user_3
が相互フォローしている状態だとしましょう。user_0
がnameを更新しました。すると4つのノードに対して更新をする必要があります。
また、複数のノードを更新する際にトランザクションをしていたとしたらデータベースの性能をとんでもなく悪化させているはずです。
どうやらこの方法でフォロー機能を実現するのはやめた方が良さそうです。
ネスト参照型
冗長化の課題を解決したネスト参照型。これもFirebase Castで紹介されています。
user_0
がuser_1
をフォローしている状態を表すためにfollowers
にuser_1
の参照だけを置く。
メリット | デメリット |
---|---|
データがスッキリ | 読み込み遅い、上位ノードでデータ量の取得量が増加していく |
FirebaseではClient Side Joinが基本です。user_0
からuser_1
を取得するためにはまずfollowersからuser_1
を取得して改めてそのkeyを持ってuser_1
を取得するため、2度通信する必要があります。
Client Side Joinについてはこちらから
https://qiita.com/1amageek/items/afc1c0ceb15ffc2372fd
どうやら良さそうですが、やはり問題があります。followersが10000件を超えた場合を考えてください。上記で述べているようにFirebase Realtime Databaseの課題にぶち当たります。user_0
以下のデータを取得しようとすると膨大な量のデータをサーバーから受け取ることになり、性能が悪化していきます。
実際、数千件程度では対して悪化はしませんので、そんなに大きくない規模のシステムならこれでも大丈夫です。
リレーションシップ参照型
データ量重くなる問題を解消したリレーションシップ参照型。user
ノードに膨れ上がるデータを置いて置くのはやめて別のノードを準備するのがこの方法です。
メリット | デメリット |
---|---|
データがスッキリ | 読み込み遅い |
フォロワーが増えていたっとしてもuser_0
のデータ量は増えません。どうやらこの方法が良さそうです。
アプリの機能について考える
では次に、簡単な写真アプリについて考えて見ましょう。
このアプリの仕様はとってもシンプルでユーザーが写真をFirebaseにアップロードできるアプリです。そして写真を見ることができるのはアップロードしたユーザーだけです。
User
とPhoto
というモデルでアプリを作って見ましょう。もちろん__リレーションシップ参照型__を使ったほうが良さそうです。
シンプルでいいアプリになりそうですがもう少し機能を追加したいので複数のユーザーで写真を共有できるグループ機能を追加しましょう。
Group
というModelを追加して複数ユーザーで写真が見えるようにしましょう。
良さそうですね。
Firestoreならもっと簡単にできるよね?
Firestoreでこのアプリを作るならどうすればいいでしょうか。Firestoreでも3つの方法があります。
- Query型
- Collection値型
- Collection参照型
Query型
FirestoreにはQueryがあります。Firebase Realtime Databaseでは1:N
のリレーションシップを使ってあらゆるデータ構造を表現するしかなかったのに対し、N:N
のデータ構造を持つことが可能になったことを意味しています。
例えば上記のフォロー機能に関して考えると以下のようにして表現できます。
また、アプリの話に戻すと__リレーションシップ参照型__にしなくともQueryを使ってowner == user_0
を取得すればUser
が保持しているPhoto
を簡単に取得することができるようになりました。
しかし、グループ機能を追加しようと思うとこの方法には問題があることがわかります。なぜならPhoto
はowner
を保持していますがgroup
を保持していません。
やはり__WHERE__に頼る開発よりもデータ構造を持たせた方が柔軟なアプリが構築できそうです。
Collection値型
User
が所持している写真なのであればUser
のSubCollectionとしてPhoto
を扱う方法です。
FirestoreになってDataとCollectionが分離させたため、User
の下にPhoto
を入れてもデータ量が増加する問題もありません。グループ機能について考えて見ましょう。Group
がPhoto
のデータを取得するためには/user/user_0/photo/photo_0
のようにuser_0
, photo_0
の二つの情報が必要になりました。Collection値型では、Keyではなくパスで管理した方が良さそうです。
Collection参照型
ネスト参照型のFirestoreバージョンがCollection参照型です。ネスト参照型の問題は取得するデータ量が増加することでしたが、Firestoreではその問題もありません。
グループ機能について考えて見ましょう。Collection値型に比べ、Photo
のデータを参照として保持し、/photo/photo_0
としてアクセスできることから考えることが減りそうです。
__Collection値型__と__Collection参照型__は大差ない
Keyで参照を保持するかパスで保持するかの違いのように見えます。あえてパスを持たせるくらいならCollection参照型にしておきましょう。でも値型には何かメリットがありそうです。値型のQueryをかけることにあります。
ここでアプリの機能を追加しましょう。見たい写真の月を入力するとその月の写真が表示されるフィルター機能を追加することにします。
自分の写真にフィルター機能を追加する
Collection値型はUser
がPhoto
を保持しているのでQueryが適応できそうです。
Collection参照型もPhoto
はowner
を保持しているのでQueryが適応できそうです。
グループの写真にフィルター機能を追加する
Collection値型のGroup
にはPhoto
のパスを保持しているだけなので実現できません。
Collection参照型もPhoto
はowner
を保持しているだけなので実現できません。
つまりQueryを適応できるデータ構造を作るのならばデータ構造を冗長型に移行する必要があります。これは現実的でしょうか?
残念ながら現実的な解ではなさそうです。上記にも記載がある通り、複数のノードを同時に更新する必要がありスケーラブルではないからです。
__Query__を諦めましょう。え!?っと思った方もいるかも知れませんが大丈夫です。どうせ書くならもっと柔軟にQueryを書けた方がいいですよね? ElasticSearchを使いましょう。
ただし、これはPhoto
が他のユーザーから参照される場合の話です。他から参照されないのであればCollection値型の方にすべきでしょう。
アプリにするならば、Collection参照型
かリレーションシップ参照型
が良さそうです。
Collection参照型
とリレーションシップ参照型
は実は全く同じことをしています。
ここで大きな問題に直面しました。SubCollection
とQuery
いつ使うの問題です。困った。
困った方はこちらへ
Cloud Firestoreを実践投入するにあたって考えたこと
Cloud FirestoreのQueryの使いどころを書いております。