Cloud FirestoreとFirebase Cloud Storageを使ってソーシャル機能を実装する方法

## Cloud FirestoreとFirebase Cloud Storageを使ってソーシャル機能を実装する方法

こんにちは、[Stamp Inc](https://stamp.team/)の[nori](https://twitter.com/1amageek)です。
金曜日の20時くらいからこの記事を書き始めて若干の後悔を感じながら記事を書きました。書き終わったら飲みに行きます。

この記事ではCloud FirestoreとFirebase Cloud Storageを使ってソーシャル機能を実装する方法を紹介します。

どの機能においてもですが、実装方法は__要件と照らし合わせて考えることが重要__です。ソーシャル機能も要件によって実装方法は異なってきます。要件によって実装がどう異なるかについても説明しようと思いますので最後まで読んで頂けると幸いです。
また、ここでのソーシャル機能とは__フォロー、フォロワー__に限定して話を進めます。

## ソーシャル機能

早速ですが、フォロー、フォロワーをCloud Firestoreではどのように表現するか見ていきましょう。
__Query__と__SubCollection__が利用可能なCloud Firestoreでは2種類の実装方法が考えられます。

##### Queryでソーシャル機能を実装
```
フォロワーを表す
/social/:social_id
```

```typescript
// Document data
{
  "createdAt": Timestamp,
  "updatedAt": Timestamp,
  "from": {
    "id": "USER_ID",
    "name": "USER_NAME",
    "thumbnail": "STORAGE_REFERENCE_PATH"
  },
  "to": {
    "id": "USER_ID",
    "name": "USER_NAME",
    "thumbnail": "STORAGE_REFERENCE_PATH"
  }
}
```


##### SubCollectionでソーシャル機能を実装
```
フォロワーを表す
/social/:followee_id/followers/:follower_id

フォローしてる人を表す
/social/:follower_id/followings/:followee_id
```

```typescript
// Document data
{
  "createdAt": Timestamp,
  "updatedAt": Timestamp,
  "name": "USER_NAME",
  "thumbnail": "STORAGE_REFERENCE_PATH"
}
```

### 2つの実装方法の違い
~~上記の二つの方法に大差はありません。 Read/Writeを考えた時に少しだけ差が出るので説明します。~~

2つの実装方法にWrite性能に大きな差が出ます。

#### WriteとReadを考える
Writeでは__SubCollection__の場合は`WriteBatch`を使って、2つのReferenceにDocumentを書き込む必要で__Query__の方は不要です。
問題になるのは、連続的なトラフィックが起こった場合です。Collectionへのインサートは秒間500回に制限されています。RootCollectionに`Social`Documentを配置した場合は性能が制限されるので__SubCollection__へ配置した方がいいでしょう。

Readでは__Query__を使う方には`where`が必要で、__SubCollection__の方は不要です。
`where`のレンジ表現に制限があるCloud Firestoreでは可能な限り`where`を使わずにかける__SubCollection__の方が若干優位かなとは思ってます。

以上のことから__SubCollection__の実装方法をオススメします。


## DataのFieldについて考える
次にFieldについて考えていきましょう。今回はシンプルな__SubCollection__を用いた場合のFieldについて説明します。Fieldについても2種類の方法を紹介します。

##### JOINを使う実装

|Field|Description|
| --- | --- |
|createdAt| データが作られた時間を記録 |
|isAvailable| 有効フラグ |
|isFollowing|フォローフラグ|

##### JOINを使わない実装

|Field|Description|
| --- | --- |
|createdAt| データが作られた時間を記録 |
|updatedAt| データが更新された時間を記録|
|name| ユーザー名 |
|thumbnail| ユーザーのサムネイル |

### 2つの実装方法の違い
2つの実装では、Readが大きく異なります。どちらの方が適しているのかは要件によって変化すると思います。
InstagramとTwitterのフォロワーリストを比べて見るとInstagramでは表示されているデータが少ないのに対して、Twitterでは多くの情報が記載されているのがわかります。微妙な違いですが、データ構造やデータの取得方法には大きく影響します。

__Instagram__
<img width="465" alt="スクリーンショット 2019-05-17 21.44.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80287/30e7a64e-2f1c-34a2-57a2-ad07e7209e93.png">

__Twitter__
<img width="473" alt="スクリーンショット 2019-05-17 21.46.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80287/8685a771-e3f6-871b-d3a2-bb9cb046771f.png">


#### JOINを使う実装のメリット・デメリット
JOINを使う実装のメリットはなんと言っても新鮮なデータであること、デメリットはN+1のReadが発生すること😱


#### JOINを使わない実装のメリット・デメリット
メリットはReadが一回で終わること、デメリットは更新頻度が低いデータしか入れてはいけないことです。

### 両者とも優位性はある。
前者のN+1問題が起こる実装なんて考えられないと思うかも知れませんが、Cloud FirestoreのクライアントSDKではCacheが準備されています。
また、フォロワーリストの表示順が最新順であるとするならばデータはローカルにある可能性が高いと考えると通信はさほど発生しない可能性の方が高く、Readについて気にする必要はないかも知れません。例えばiOSのデフォルトでは100MBが割り当てられいるため、なかなかのデータがローカルに保存されています。またフォロワーを確認するタスクがメインとなるアプリはないはずなので、`N+1 ≒ 100`くらいと考えてしまってもいいかも知れません。

後者の実装を考えるときユーザーデータの更新頻度は低く、一般的なユーザーのフォロワー数は数百人であることが予想できます、全てのフォロワーのデータを更新したとしても大きな負荷ではありません。
しかし、もしユーザーデータが頻繁に更新される場合では`(Nmax+1)x更新回数`Writeが発生することになるので、どちらの実装にするのかは要件に合わせてください。

更新されないデータのみを入れておくのも手段の一つだと思います。

### Firebase Cloud Storageを組み合わせる
Fieldにある`thumbnail`にはFirebase Cloud StorageのPathが入ることを想定しています。このPathには以下のようにフォーマットを固定するといいと思います。

```
/user/:user_id/thumbnail
```

個人的にはCloud Firestoreの構造とStorageの構造を一致させることは非常に効果的で、開発時に楽になりますし、セキュリティルールの記載も楽になります。また`thumbnail`など特定の場所に使う場合のファイル名は固定するといいでしょう。こうすることで画像データの差し替えではDocument dataの更新は行わずに済みます。


---

Cloud FirestoreとCloud Storageをいい感じで連携できるライブラリ作りました。
iOSとTypeScriptで利用可能です。ぜひ試してみてください！！

![Ballcap-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80287/ab29cbba-c92f-bb9b-44d7-3e2b51eaf682.png)


https://github.com/1amageek/Ballcap-iOS
https://github.com/1amageek/ballcap.ts

それでは👍🏻