1. 1amageek

    No comment

    1amageek
Changes in body
Source | HTML | Preview
@@ -1,391 +1,392 @@
## はじめに
[Firebase Realtime DBを実践投入するにあたって考えたこと](https://qiita.com/1amageek/items/64bf85ec2cf1613cf507)を読んで頂いてありがとうございます。 多くの方から「いいね」を頂いて、今回のこの記事を書くモチベーションになりました:bow_tone1:
本当にありがとうございました!
さて、[CloudFirestore](https://firebase.google.com/docs/firestore/?hl=ja)は、Firebase Realtime Databaseとは全く違うデータベースです。特に`SubCollection`や`Query`が導入されたことにより、リレーションシップの設計に関して大きく異なります。
この記事では、主にCloudFirestoreにおけるリレーションシップの設計方法から、アプリ・CloudFunctionsに至るまでを幅広く解説して行こうと思います。
### Cloud Firestoreでの開発について
私の経験上確実に断言できることがあります。
__Cloud Firestoreだけでサービスを作ることは不可能ではない__
でもしんどい。
開発には、他のSaaSを活用にするのがいいと思います。マイクロサービスをつくる観点から考えても機能を分離しておくことは大きなメリットがあります。
もし今から新規サービスを作ろうとして技術選定に困っている方にアドバイスするならば、私はCloud Firestoreを強くお勧めします。簡単に理由を並べると以下の点です。
- 既存のDBと比較して、今からNoSQLを始める学習コストを考慮しても開発速度が早い
- スケールするまでは無料で使える
- グロースさせるまでをFirebaseで完結できる
正直、ネイティブアプリからREST APIを使ってデータを取り扱うメリットはほぼ無いと考えています。完全私の予想ですが、次のような流れになるはずです。
- 通信プロトコルはgRPCが主流になる
- RESTはGraphQLに置き換わる
- RESTは外部サービスとの連携のために残る
完全に個人的な予想なのであまり期待しない方がいいかも知れませんが、僕はそう信じてこの記事を書きます。
## Cloud Firestoreの構造
Cloud Firestore は、NoSQLデータベースです。さらに特徴的なのはデータ構造です。
図のようにPCのファイルシステムのような構造を持つことができます。
![structure-data.png](https://qiita-image-store.s3.amazonaws.com/0/80287/521ef355-9558-0ba1-7edb-c5b1721b2fa6.png)
このあたりの説明は丁寧にドキュメントで説明されているので[こちら](https://firebase.google.com/docs/firestore/?hl=ja)をご覧ください。
一般的なRDBでもなく、MongoDBのよな構造でもなく、CloudFirestoreは独特の構造を持ちますので、雰囲気だけでも構造を理解してこの後を読み進めることをお勧めします。
## CloudFirestoreのリレーションシップについて
さて、Cloud Firestoreでサービス開発において重要なのはCloudFirestoreのデータ構造をどう設計していくかです。もちろん__リレーションシップ__の設計が重要な鍵となります。Realtime Databaseでは、リレーションシップの方法はそう多くなく、__Fan out__によるリレーションシップを構築していく程度でしたが、Cloud Firestoreでは違います。`Query` `SubCollection` `Reference`などリレーションを行う方法が複数用意されているからです。
NoSQLのベストプラクティスは資料が本当に少なくて色々考えるのに苦労したんですが、僕が考えたベストプラクティスをみんな見てください。__そして指摘があればください。__
参考になりそうな資料を載せておきます。
[DynamoDB のベストプラクティス](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/best-practices.html)
サーバーやインフラの性能に触れながら読めるのでとてもいい資料です。
[NoSQLデータモデリング技法](https://gist.github.com/matope/2396234)
Realtime Databaseを設計するなら必ず読んだ方がいい資料です。
残念ながらこれらより時代は進化してまして。
`SubCollection`について考慮された資料は公式のFirebaseがリリースしている情報をのぞいて皆無に近い状態です。
[YouTube Firebase](https://www.youtube.com/channel/UCP4bf6IHJJQehibu6ai__cg)をご参照ください。
## CloudFirestore データベース設計
2018年の[DevFest](https://tokyo2018.gdgjapan.org/)で登壇した資料をより深く解説します。🙏🏻
[Firestore Database Design](https://speakerdeck.com/1amageek/firestore-database-design)
### リレーションシップの種類
Cloud Firestoreの複数の方法でリレーションシップ作ることが可能です。まずはその種類を紹介します。タイプ別に種類を図にしました。
<img width="797" alt="スクリーンショット 2018-11-30 13.06.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/a4a29a93-f902-cec9-b293-721a4d007d15.png">
最終的にこの8パターン組み合わせになるのかなと考えています。
*以降Swiftのコードが掲載されますなんとなく読めると思うのでご参考ください。*
#### ■ Key
<img width="734" alt="スクリーンショット 2018-11-30 13.19.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/eec1445b-b072-e128-a706-5a47bd4ffdbc.png">
これはRDBでも使われる一般的なリレーション方法です。RDBで言うならテーブルに参照先のレコードのIDを持っている状態です。ここでは、`Item`が`userID`を保持していることから`Item`と`User`の関係を表しています。
#### ■ Reference
<img width="737" alt="スクリーンショット 2018-11-30 13.38.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/5609338d-12e4-518f-a1ee-2b198d26063e.png">
これはCloud Firestoreがもつ__Reference__型を使ったリレーション方法です。
__Key__との違いについて考えてみましょう。CloudFirestoreでは次のようにパスにJSONデータを持たせます。
```typescript
// /user/:id
{
"name": "hoge",
"age": "25"
}
```
ユーザーの情報のマイグレーションしなければならない状態を想定しましょう。例えば`age`をStringで定義してしまったのでNumberに変更したい場合、今の構造では次のようにするしかなくなります。
```typescript
// /user/:id
{
"name": "hoge",
"age": "25"
"age_number": 25
}
```
ちょっと残念ですよね。 ちなみにこれ__ベストプラクティス__です。色々考慮するとこのマイグレーションが一番コストかからずシンプルに移行できます。
ちょっと残念だから綺麗にしたい方はこうするのがオススメです。
```typescript
// /version/1/user/:id
{
"name": "hoge",
"age": "25"
}
```
最初からパスにバージョン情報を持たせましょう。そうすると
```typescript
// /version/2/user/:id
{
"name": "hoge",
"age": 25
}
```
バージョンの変更に合わせて、データをマイグレーションできます。増大したデータのマイグレーションにはコストもかかるので、モデルのバージョンをあげることは稀ですが可能です。
しかし、ここでリレーションに話を戻すと問題が出てきます。
`Item`と`User`の関係を表す`userID`はIDのみを保持しており、バージョン情報を持っていません。そこで登場するのが`Reference`になります。`Reference`は__パス__そのものを保持することが出来るようになります。
##### Referenceは多用できない
「Reference便利💪🏻」となったかも知れませんが、`Reference`は多用できません。なぜでしょうか?ItemにReferenceを持たせるとどうなるかを考えてみましょう。
次の状態では、`Item`はバージョン`1`の`User`を参照しています。
```typescript
// /version/1/item/:id
{
"userID": "user_ID" // :id
"userReference": "<Ref>", // /version/1/user/:id
}
```
もし`User`のバージョンが更新さたらどうなるでしょうか?`Item`は古いバージョンの`Reference`を持っているため`Item`もマイグレーションが必要になります。
どうやら違うモデルを参照する場合は、__Key__のみを保持する方が良さそうです。
ではReferenceはいつ使うのか?
- 新しいモデルから古いモデルを参照する時
- ネストの深いモデルを参照する時
ではないかと考えています。
例えば次のように現行バージョンが旧バージョンを参照する場合や
```typescript
// /version/2/item/:id
{
"userID": "user_ID" // :id
"oldItem": "<Ref>", // /version/1/item/:id
}
```
Keyでは表現しきれない階層の任意の情報を示したい場合
```typescript
// /version/1/user/:id
{
"userID": "user_ID" // :id
"pinComment": "<Ref>", // /version/1/item/:item_id/comment/:comment_id
}
// /version/1/item/:item_id/comment/:comment_id
{
"userID": "user_ID" // :id
"oldItem": "<Ref>", // /version/1/item/:id
}
```
#### ■ Same ID
<img width="718" alt="スクリーンショット 2018-11-30 14.37.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/98551c4a-00bf-23a6-242f-ee095dfe1826.png">
このリレーション方法は、CloudFirestoreのパス構造を使った方法です。セキュリティールールを効率的に与えることができるのでセキュアなデータを扱いたいときにオススメです。ユーザーにセキュアな情報を持たせたい場合を考えてみましょう。CloudFirestoreのセキュリティルールではフィールド単位でセキュリティをかけることができません。つまり高いセキュリティを持つドキュメントと公開可能なドキュメントは別々に保持する必要があります。例えばユーザーの情報のセキュリティを高く保つための構造は以下の二つの方法が考えられると思います。
##### 1. SubCollectionを利用した構造
`/version/1/user/:user_id/secure/:id`
UserのSubCollectionにセキュリテイを高めたいデータを持ちます。
SubCollectionのセキュリティルールを設定しデータをセキュアに保ちます。
##### 2. Same ID構造
`/version/1/user/:user_id`
`/version/1/_user/:user_id`
上では`User`と`_User`の別のCollectionを定義しています。__Same ID__を利用してシンプルにセキュアな情報を保持できます。
#### ■ Query
<img width="724" alt="スクリーンショット 2018-11-30 16.03.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/0b775748-bd3f-f403-6a21-bef588cee38d.png">
このリレーションの方法もRDBでも使われる一般的な方法です。RTDBとは違い`where`が利用できるようになってとても便利になりました。
#### ■ Sub Collection
<img width="728" alt="スクリーンショット 2018-11-30 15.48.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/fea5609d-c1c4-e0bf-27ff-6297165ff0de.png">
Cloud Firestore最大の特徴がこの`SubCollection`です。この構造は他のデータベースには存在しません。
##### QueryとSubCollectionの使い分け
ここでQueryとSubCollectionの使い分けについて考えてみましょう。
[Cloud FirestoreのSubCollectionとQueryっていつ使うの問題](https://qiita.com/1amageek/items/d2ef7a49bccf5b4ea78e)
過去のこの様な記事をリリースしましたが、この問題の解答編になります。
| - | メリット | デメリット |
|---|---|---|
|Query| 横断的に検索を行える | Readのセキュリティルールは必ず全員に公開する必要がある |
|SubCollection| セキュリティが高い | ネストしている親をまたいで検索は行えない |
`横断的に検索を行える`の説明をしておきます。
Cloud Firestoreではドキュメントに強固なセキュリティルールを設定した場合にQueryを実行することができなくなります。つまりRoot Collectionに配置するドキュメントは緩やかなセキュリティルールを持っている必要があります。
__セキュリティが使い方を分ける__
2つのリレーション方法の使い分けはセキュリティに依存します。
サービスの全利用者に公開できる情報はRoot Collectionとして`Query`でリレーションするのが良さそうです。一定のセキュリティを保ちたい情報はSubCollectionにする方が良さそうです。
例えば
- ブログの記事などの公開情報はルートコレクションへ
- 決済情報などセキュアな情報はUserのSubCollectionへ
#### ■ Junction Collection
<img width="717" alt="スクリーンショット 2018-11-30 16.56.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/1c9abb67-3d92-2b15-de82-eb3ebd825de5.png">
この方法もRDBで使われる中間テーブルをもつ方法です。
#### ■ Reference Collection
<img width="715" alt="スクリーンショット 2018-11-30 17.05.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/ea71df93-7322-6af7-9364-975d215822a2.png">
この方法はCloud FirestoreのSubCollectionを利用した方法です。
NoSQLで唯一Cloud Firestoreだけが出来る構成です。
__Firebase Realtime Database__で、この方法を使うと構造上データが肥大化することになり使うことが出来ませんでした。Cloud Firestoreでは、DocumentとCollectionに分離された構造になっているため、その制約がなくなりました。
##### Junction CollectionとReference Collectionの使い分け
| - | メリット | デメリット |
|---|---|---|
|Junction Collection| 横断的にリレーションシップの検索を行える<br>状態を持てる | Readのセキュリティルールは必ず全員に公開する必要がある |
|ReferenceCollection| リレーションシップを持っている状態を隠せる | ネストしている親をまたいで検索は行えない |
N:Nのリレーションシップにおいても、セキュリティルールに依存します。
例えば、`招待機能`の機能を考えてみましょう。
>`User A`から`User B`に送られた招待状が`未開封`のままである。
これをJunction Collectionのデータにするならば下のようになります。
```typescript
// Invitation
{
"fromID": "userA",
"toID": "userB",
"status": "isUnopened"
}
```
次に、この招待状を受け入れるとそれぞれフォロー関係が成り立つとしましょう。
これを構造で表すと下のようになります。
`/user/userA/followers/userB`
`/user/userB/followers/userA`
`User A`が`User B`を`followers`として保持し、`User B`も`User A`を`followers`として保持する。
Firestoreでは`WriteBatch`を使って複数の書き込み先に同時に一度に書き込むことが可能なので、どの処理も簡単に行えます。
[トランザクションと一括書き込み](https://firebase.google.com/docs/firestore/manage-data/transactions?hl=ja)
+[Firestotre のバッチ処理とトランザクション処理](https://qiita.com/samuraikun/items/ce5d977d63bcafa43d0e#_reference-ea6c96f3b8492264399c)
#### ■ Duplicated Collection
<img width="715" alt="スクリーンショット 2018-11-30 17.54.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/6281d674-e4ea-7bca-45b4-6dbf356168ee.png">
この方法は、セキュリティを保ちつつデータを参照するデータを参照する方法です。
決済情報をもつデータ構成を考えてみましょう。
> `Shop`の`Product`を`User`が購入した情報を`Transaction`として保持する。
> `Transaction`は`Shop`と`User`のみが参照できる。
この要件を満足したい時、`Transaction`はどこに保持するのがいいでしょうか。
まず、`Transaction`には必ずセキュリティルールを設定するので、Queryは機能しなくなります、そのためルートコレクションに配置することは避けた方が良さそうです。
次に、セキュリティを担保するためにはSubCollection構造にするのが良さそうですが、`Shop`と`User`のどちらに持たせるのがいいでしょうか?`User`が持たせると`Shop`からは参照できませんし、`Shop`に持たせると`User`からは参照できません。
ということで双方に保持するようにしましょう。これも__WriteBatch__を利用することで簡単に実現可能です。ただしこの方法は、Transactionのように書き換え頻度が低いデータに限った方が良さそうです。
__冗長化すべきデータの制限__
次のデータ構造を考えてみましょう。 ユーザーの情報を冗長化してフォローに保存しています。
```typescript
// /user/:user_id
{
"name": "hoge",
"location": [0, 0],
"age": "25"
}
// /user/:user_id/followers/:id
{
"name": "hoge",
"location": [0, 0],
"age": "25"
}
```
ユーザー情報は高頻度で更新されることが予想されます。ユーザーの情報が更新される度にフォロー先のデータを更新するのはとてもいい設計とは言えません。
私が実戦で利用しているデータ構造を8個ご紹介しましたが、他にもあらゆる構成が考えられます。見つけたらぜひ教えてください。
# アプリで考慮すること
## REST API
Cloud Firestoreは、SDKを利用することでDBに直接書き込みができます。一方でCloud Functionsを使うことでREST APIを設けCloud Functions経由でDBに書き込むことも可能です。
ではどちらを使えばいいのか考えてみましょう。
__セキュリティについて__
セキュリティ的にはREST APIでもSDKであっても大きな差はありません。ただSDKではセキュリティルールだけ考慮すればいいのに対して、REST APIではCloud Functionsの中で全てをAdminで動かすことになりますのでAPIのセキュリティには注意をする必要があります。
__実装工数__
プロトタイピングなどではSDKを利用する方が圧倒的に実装工数を低減できます。ただし、セキュリティルールを最低限にしてるものに限ります。個人的にはプロトタイピングの段階で強固なセキュリティルールは必要ないと思っているので、最低限のルールを記載して開発を進めるのがいいでしょう。
__Callable Functions__
Firebaseには`Callable functions`と呼ばれる専用のAPIが準備されています。このAPIにはAuth情報も含まれているのでREST APIを作るよりも安全に実装することが可能です。
REST APIを外部から呼ぶことが内容であればREST APIは利用せず`Callable Functions`を利用することがいいでしょう。
### Callable Functions vs SDK
Cloud Functionsに処理を任せることの最大のメリットはセキュリティルールをバイパスする事です。運用が開始され、セキュリティルールを強固にしていった時必ず権限の持たせ方に困ることがあります。例えば先ほどの紹介した`Duplicated Collection`では必ず相手の保護された領域に書き込みを行うことになります。となるとCloud Functionsを経由するのは必須となります。
また、SDKではセキュリティを考慮せず書き込みが行われる場合に活用するのがいいでしょう。やはりFirebaseの開発の醍醐味は開発の高速化にあると思いますので、あえてAPIを利用したくていいのであれば可能な限りこちらを使うのが得策であると考えています。
# Cloud Firestore Best Practice
## Killswitch
Firebase Realtime Databaseと同様に、Cloud Firestoreは開発側の都合でサーバーを停止することはできません。必ずクライアントからの利用を制限する機能を設けましょう。
KillSwitch自体をCloud Firestoreに持たせる事も可能です。
例えば下の図のように強制アップデートが必要なバージョンやアプリが利用可能かを示すフラグを持たせることでハンドリングしましょう。
<img width="1204" alt="スクリーンショット 2018-12-01 9.56.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/61ca64d7-f88b-3c97-bf00-b13b30125628.png">
## Model
### Model設計
RDBを利用してきたエンジニアでばあるほど、NoSQLの設計には苦労します。僕の周りの人間も実際にそうです。NoSQLの設計には割り切りも必要ですし、テクニックを知っている必要がありますが、Cloud FirestoreへQueryが実装されたこともあり、ある程度RDBで利用されてきた考え方が通用します。まずは、SubCollectionについて考えるのではなくRoot CollectionにModelを配置し、プロトタイピングを行ってみましょう。データ取得の最適化が必要なポイントはそこで整理できますし、セキュリティルールを強固にする必要があるポイントも見えてくるはずです。
### Model設計の制約
#### ■ Modelは並列に構成する
SubCollectionが準備されたことで、ネスト構造こそがCloud Firestoreの真骨頂のように見えるかも知れませんが、あくまでNoSQLデータベースの欠点を補う機能にすぎません。NoSQLのデータベース設計を理解し、効率的にSubCollectionを活用しましょう。
#### ■ Modelは`updatedAt`, `createdAt`を保持する
もはやアプリ開発系の慣例的な部分でもありますが、やはりこの情報は持っておくことはすごく重要です。
運用時にも役にたちますし、開発においてもソートで利用することは結構あります。
#### ■ Model内のArrayを活用する
ここはFirebase Realtime Databaseと全く逆の考えになるの注意してください。Cloud Firestoreでは、Arrayの制御も追加されました。ArrayをQueryで利用することも可能なので積極的にArrayを利用しましょう。
[Better Arrays in Cloud Firestore!](https://firebase.googleblog.com/2018/08/better-arrays-in-cloud-firestore.html)
#### ■ Model内にパーミッションを持たせる
`public` `private`などのパーミッションを持たせることで、セキュリティルールのハンドリング簡単に行うことが可能になります。
<img width="587" alt="スクリーンショット 2018-12-01 10.21.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/0aa2f3be-4e8f-425e-152a-d8bdba33e0ca.png">
Firebase Summit 2018のセッションでも詳しく解説されているのでこちらをご参照ください。
https://www.youtube.com/watch?v=pvLkkLjHdkw&index=6&list=PLl-K7zZEsYLnqdlmz7iFe9Lb6cRU3Nv4R
## 最後に
Firebaseにおいての上記の設計思想からModelを管理できるLibraryを作りました。
ライブラリの利用実績も増えて行ってます!ぜひ利用してください!!コントリビューターも募集しております!
__Pring for iOS__
https://github.com/1amageek/Pring
__Pring for Cloud Functions__
https://github.com/1amageek/pring-admin.ts
__Pring for Web__
https://github.com/1amageek/pring.ts
Firebaseについてさらに詳しく知りたい方は次をご覧ください。
- [Firebase Realtime Databaseとはなんなのか?](https://qiita.com/1amageek/items/b350ee5ef0c9b2406583)
- [Firebaseでアプリを開発するならClient Side Joinを前提にすること](https://qiita.com/1amageek/items/afc1c0ceb15ffc2372fd)
- [あなたが知らないFirestoreのコアテクノロジー](https://qiita.com/1amageek/items/175305687acbe39b47d9)