Cloud FirestoreのSubCollectionとQueryっていつ使うの問題

# SubCollectionいつ使うの問題

Cloud Firestoreがリリースされて数日経ちました。[Salada](https://github.com/1amageek/Salada)をFirestoreに対応させるため、仕様の深いところまで検証しているところです。そんな中でぶち当たった問題について記載します。

## TL;DR
- Queryに依存するアプリを作ると拡張性を失う。
- QueryはElasticSearchに任せる。
- SubCollectionの使い所は限定的。


## SubCollectionとは

Firestoreは`Collection`と`Document`と`Data`で構成されます。
Collectionは複数のDocumentを持つことができ、
DocumentはDataとCollectionを持つことができます。Documentが持つCollectionのことをSubCollectionと言います。

![structure-data.png](https://qiita-image-store.s3.amazonaws.com/0/80287/2d538dcb-28a2-0018-0b5a-5c34118a587f.png)

![unnamed.png](https://qiita-image-store.s3.amazonaws.com/0/80287/f59693d3-7de3-96ef-4fd2-4c4fcec9c4a2.png)

## なぜSubCollectionが必要だったのか

Firebase Realtime Databaseでは深いネストの上位ノードでデータを取得すると、そのノード以下全てを取得してしまう課題がありました。

例えば`v1`でデータを取得すると`follower`も`user`も全部取得しちゃう。通信がえげつないことになる訳です。

<img width="425" alt="スクリーンショット 2017-10-07 10.57.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/d8dda442-f532-ee5e-5177-bc38052d9cdb.png">

そこで登場したのが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`ごと入れ込んじゃう。

<img width="318" alt="スクリーンショット 2017-10-07 11.51.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/1437e2a5-5a9d-bc45-2374-966f0c9edff8.png">


|メリット|デメリット|
|---|---|
|読出し高速|データ量増加<br>複数のノードに同時に書き込む|

`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`の参照だけを置く。


<img width="328" alt="スクリーンショット 2017-10-07 12.05.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/f261705a-6408-3e5c-b4fe-f0ea6dbc2899.png">

|メリット|デメリット|
|---|---|
|データがスッキリ|読み込み遅い、上位ノードでデータ量の取得量が増加していく|

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`ノードに膨れ上がるデータを置いて置くのはやめて別のノードを準備するのがこの方法です。

<img width="250" alt="スクリーンショット 2017-10-07 12.26.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/fb033d3e-6869-dc8c-3871-b8a5cb9662be.png">


|メリット|デメリット|
|---|---|
|データがスッキリ|読み込み遅い|

フォロワーが増えていたっとしても`user_0`のデータ量は増えません。どうやらこの方法が良さそうです。

## アプリの機能について考える

では次に、簡単な写真アプリについて考えて見ましょう。
このアプリの仕様はとってもシンプルでユーザーが写真をFirebaseにアップロードできるアプリです。そして写真を見ることができるのはアップロードしたユーザーだけです。

`User`と`Photo`というモデルでアプリを作って見ましょう。もちろん__リレーションシップ参照型__を使ったほうが良さそうです。

<img width="255" alt="スクリーンショット 2017-10-07 12.38.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/d6f2fee2-6ab3-8e04-2dc3-61c46164af0a.png">

シンプルでいいアプリになりそうですがもう少し機能を追加したいので複数のユーザーで写真を共有できるグループ機能を追加しましょう。
`Group`というModelを追加して複数ユーザーで写真が見えるようにしましょう。

<img width="345" alt="スクリーンショット 2017-10-07 12.50.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/482154f1-fc63-bcd4-25c8-24889be86f60.png">

良さそうですね。

## Firestoreならもっと簡単にできるよね？

Firestoreでこのアプリを作るならどうすればいいでしょうか。Firestoreでも3つの方法があります。

- Query型
- Collection値型
- Collection参照型

### Query型

FirestoreにはQueryがあります。Firebase Realtime Databaseでは`1:N`のリレーションシップを使ってあらゆるデータ構造を表現するしかなかったのに対し、`N:N`のデータ構造を持つことが可能になったことを意味しています。
例えば上記のフォロー機能に関して考えると以下のようにして表現できます。

<img width="295" alt="スクリーンショット 2017-10-07 15.07.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/46b91359-304f-8932-c4d4-c5e4f6a4df88.png">


また、アプリの話に戻すと__リレーションシップ参照型__にしなくとも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](https://www.elastic.co/jp/products/elasticsearch)を使いましょう。
ただし、これは`Photo`が他のユーザーから参照される場合の話です。他から参照されないのであればCollection値型の方にすべきでしょう。


アプリにするならば、`Collection参照型`か`リレーションシップ参照型`が良さそうです。
`Collection参照型`と`リレーションシップ参照型`は実は全く同じことをしています。

ここで大きな問題に直面しました。`SubCollection`と`Query`いつ使うの問題です。困った。

---

困った方はこちらへ
[Cloud Firestoreを実践投入するにあたって考えたこと](https://qiita.com/1amageek/items/d606dcee9fbcf21eeec6)

Cloud FirestoreのQueryの使いどころを書いております。