はじめに
Firebase Realtime DBを実践投入するにあたって考えたことを読んで頂いてありがとうございます。 多くの方から「いいね」を頂いて、今回のこの記事を書くモチベーションになりました
本当にありがとうございました!
さて、CloudFirestoreは、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のファイルシステムのような構造を持つことができます。
このあたりの説明は丁寧にドキュメントで説明されているのでこちらをご覧ください。
一般的なRDBでもなく、MongoDBのような構造でもなく、CloudFirestoreは独特の構造を持ちますので、雰囲気だけでも構造を理解してこの後を読み進めることをお勧めします。
CloudFirestoreのリレーションシップについて
さて、Cloud Firestoreでサービス開発において重要なのはCloudFirestoreのデータ構造をどう設計していくかです。もちろんリレーションシップの設計が重要な鍵となります。Realtime Databaseでは、リレーションシップの方法はそう多くなく、Fan outによるリレーションシップを構築していく程度でしたが、Cloud Firestoreでは違います。Query
SubCollection
Reference
などリレーションを行う方法が複数用意されているからです。
NoSQLのベストプラクティスは資料が本当に少なくて色々考えるのに苦労したんですが、僕が考えたベストプラクティスをみんな見てください。そして指摘があればください。
参考になりそうな資料を載せておきます。
DynamoDB のベストプラクティス
サーバーやインフラの性能に触れながら読めるのでとてもいい資料です。
NoSQLデータモデリング技法
Realtime Databaseを設計するなら必ず読んだ方がいい資料です。
残念ながらこれらより時代は進化してまして。
SubCollection
について考慮された資料は公式のFirebaseがリリースしている情報をのぞいて皆無に近い状態です。
YouTube Firebaseをご参照ください。
CloudFirestore データベース設計
2018年のDevFestで登壇した資料をより深く解説します。🙏🏻
Firestore Database Design
リレーションシップの種類
Cloud Firestoreの複数の方法でリレーションシップ作ることが可能です。まずはその種類を紹介します。タイプ別に種類を図にしました。
最終的にこの8パターン組み合わせになるのかなと考えています。
以降Swiftのコードが掲載されますなんとなく読めると思うのでご参考ください。
■ Key
これはRDBでも使われる一般的なリレーション方法です。RDBで言うならテーブルに参照先のレコードのIDを持っている状態です。ここでは、Item
がuserID
を保持していることからItem
とUser
の関係を表しています。
■ Reference
これはCloud FirestoreがもつReference型を使ったリレーション方法です。
Keyとの違いについて考えてみましょう。CloudFirestoreでは次のようにパスにJSONデータを持たせます。
// /user/:id
{
"name": "hoge",
"age": "25"
}
ユーザーの情報のマイグレーションしなければならない状態を想定しましょう。例えばage
をStringで定義してしまったのでNumberに変更したい場合、今の構造では次のようにするしかなくなります。
// /user/:id
{
"name": "hoge",
"age": "25"
"age_number": 25
}
ちょっと残念ですよね。 ちなみにこれベストプラクティスです。色々考慮するとこのマイグレーションが一番コストかからずシンプルに移行できます。
ちょっと残念だから綺麗にしたい方はこうするのがオススメです。
// /version/1/user/:id
{
"name": "hoge",
"age": "25"
}
最初からパスにバージョン情報を持たせましょう。そうすると
// /version/2/user/:id
{
"name": "hoge",
"age": 25
}
バージョンの変更に合わせて、データをマイグレーションできます。増大したデータのマイグレーションにはコストもかかるので、モデルのバージョンをあげることは稀ですが可能です。
しかし、ここでリレーションに話を戻すと問題が出てきます。
Item
とUser
の関係を表すuserID
はIDのみを保持しており、バージョン情報を持っていません。そこで登場するのがReference
になります。Reference
はパスそのものを保持することが出来るようになります。
Referenceは多用できない
「Reference便利💪🏻」となったかも知れませんが、Reference
は多用できません。なぜでしょうか?ItemにReferenceを持たせるとどうなるかを考えてみましょう。
次の状態では、Item
はバージョン1
のUser
を参照しています。
// /version/1/item/:id
{
"userID": "user_ID" // :id
"userReference": "<Ref>", // /version/1/user/:id
}
もしUser
のバージョンが更新さたらどうなるでしょうか?Item
は古いバージョンのReference
を持っているためItem
もマイグレーションが必要になります。
どうやら違うモデルを参照する場合は、Keyのみを保持する方が良さそうです。
ではReferenceはいつ使うのか?
- 新しいモデルから古いモデルを参照する時
- ネストの深いモデルを参照する時
ではないかと考えています。
例えば次のように現行バージョンが旧バージョンを参照する場合や
// /version/2/item/:id
{
"userID": "user_ID" // :id
"oldItem": "<Ref>", // /version/1/item/:id
}
Keyでは表現しきれない階層の任意の情報を示したい場合
// /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
このリレーション方法は、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を利用してシンプルにセキュアな情報を保持できます。
Same IDを使ったソーシャル機能
/version/1/user/:user_id
/version/1/social/:user_id
User
とSocial
を別のCollectionに定義しています。
Userに定義されるであろうデータ
{
"name": "1amageek",
"age": 31,
"gender": "male"
}
Socialに定義されるであろうデータ
{
"followerCount": 2000,
"followeeCount": 23
}
一見全てユーザーがもつべきデータに見えますが、User
とSocial
には明確に分けるべき理由があります。
-
User
データは自分以外から更新させたくない -
Social
データは自分以外からの更新できるようにしたい
理由はそれだけではありません。Social
データの方が圧倒的に更新頻度が多いはずです。
ここでUser
データの特性を考えてみましょう。このデータはどこで利用されるでしょうか。ソーシャル機能を作っているのであればユーザー検索は必須な機能となり得るでしょう。Cloud Firestoreの検索機能は非常に貧弱なのでAlgoliaやElasticSearchなどに検索機能を任せることが考えられます。
同時にAlgoliaやElasticSearchの更新にはCloud Functionsを利用することが想像できると思います。もしUser
データにSocial
データを含めていたらどうなるでしょうか。カウントがインクリメントされるだけでCloud Functionsがトリガーされることになりそうです。
■ Query
このリレーションの方法もRDBでも使われる一般的な方法です。RTDBとは違いwhere
が利用できるようになってとても便利になりました。
■ Sub Collection
Cloud Firestore最大の特徴がこのSubCollection
です。この構造は他のデータベースには存在しません。
QueryとSubCollectionの使い分け
ここでQueryとSubCollectionの使い分けについて考えてみましょう。
Cloud FirestoreのSubCollectionとQueryっていつ使うの問題
過去のこの様な記事をリリースしましたが、この問題の解答編になります。
- | メリット | デメリット |
---|---|---|
Query | 横断的にQueryを行える | Readのセキュリティルールは必ず全員に公開する必要がある |
SubCollection | セキュリティの設定が容易 | ネストしている親をまたいで検索は行えない |
CollectionGroup | ネストしている親をまたいで検索を行える | 横断範囲が広くセキュリティの設定が難しい |
横断的にQueryを行える
の説明をしておきます。
Cloud Firestoreではドキュメントに強固なセキュリティルールを設定した場合にQueryを実行することができなくなります。
セキュリティルールとQueryの関係はこちらに記載されています。
https://firebase.google.com/docs/firestore/security/rules-query?hl=ja
つまりRoot Collectionに配置するドキュメントは緩やかなセキュリティルールを持っている必要があります。
2019年6月のアップデートでCloud FirestoreでCollectionGroupが利用可能になりました。
SubCollectionでも横断的にQueryを実行できるようになりました。
待ち焦がれたCollectionGroupがCloud Firestoreへやってきた。
データのインサートが多いCollectionはRootCollectionには配置しない
Collectionへのインサートには1秒間500回の制限があります。コンシューマー向けのサービスで瞬間的性能が必要なサービスを作るのであればSubCollectionを利用した方が良いでしょう。
例えば
- ECなどの売上を管理したい場合、
Transaction Document
をRootCollectionに配置してしまうと1秒に500件以上の販売することができない。/user/:user_id/transactions/:transaction_id
としてCollectionGroup
で計算する方が良さそうです。
セキュリティが使い方を分ける
2つのリレーション方法の使い分けはセキュリティに依存します。
サービスの全利用者に公開できる情報はRoot CollectionとしてQuery
でリレーションするのが良さそうです。一定のセキュリティを保ちたい情報はSubCollectionにする方が良さそうです。
例えば
- ブログの記事などの公開情報はルートコレクションへ
- 決済情報などセキュアな情報はUserのSubCollectionへ
■ Junction Collection
■ Reference Collection
この方法はCloud FirestoreのSubCollectionを利用した方法です。
NoSQLで唯一Cloud Firestoreだけが出来る構成です。
Firebase Realtime Databaseで、この方法を使うと構造上データが肥大化することになり使うことが出来ませんでした。Cloud Firestoreでは、DocumentとCollectionに分離された構造になっているため、その制約がなくなりました。
Junction CollectionとReference Collectionの使い分け
- | メリット | デメリット |
---|---|---|
Junction Collection | 横断的にリレーションシップの検索を行える 状態を持てる |
Readのセキュリティルールは必ず全員に公開する必要がある |
ReferenceCollection | リレーションシップを持っている状態を隠せる | ネストしている親をまたいで検索は行えない |
N:Nのリレーションシップにおいても、セキュリティルールに依存します。
例えば、招待機能
の機能を考えてみましょう。
User A
からUser B
に送られた招待状が未開封
のままである。
これをJunction Collectionのデータにするならば下のようになります。
// 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
を使って複数の書き込み先に同時に一度に書き込むことが可能なので、どの処理も簡単に行えます。
トランザクションと一括書き込み
Firestotre のバッチ処理とトランザクション処理
■ Duplicated Collection
この方法は、セキュリティを保ちつつデータを参照するデータを参照する方法です。
決済情報をもつデータ構成を考えてみましょう。
Shop
のProduct
をUser
が購入した情報をTransaction
として保持する。
Transaction
はShop
とUser
のみが参照できる。
この要件を満足したい時、Transaction
はどこに保持するのがいいでしょうか。
まず、Transaction
には必ずセキュリティルールを設定するので、横断的なQueryは機能しなくなります、そのためルートコレクションに配置することは避けた方が良さそうです。
次に、セキュリティを担保するためにはSubCollection構造にするのが良さそうですが、Shop
とUser
のどちらに持たせるのがいいでしょうか?User
が持たせるとShop
からは参照できませんし、Shop
に持たせるとUser
からは参照できません。
ということで双方に保持するようにしましょう。これもWriteBatchを利用することで簡単に実現可能です。ただしこの方法は、Transactionのように書き換え頻度が低いデータに限った方が良さそうです。
冗長化すべきデータの制限
次のデータ構造を考えてみましょう。 ユーザーの情報を冗長化してフォローに保存しています。
// /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に持たせる事も可能です。
例えば下の図のように強制アップデートが必要なバージョンやアプリが利用可能かを示すフラグを持たせることでハンドリングしましょう。
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!
■ Model内にパーミッションを持たせる
public
private
などのパーミッションを持たせることで、セキュリティルールのハンドリング簡単に行うことが可能になります。
Firebase Summit 2018のセッションでも詳しく解説されているのでこちらをご参照ください。
https://www.youtube.com/watch?v=pvLkkLjHdkw&index=6&list=PLl-K7zZEsYLnqdlmz7iFe9Lb6cRU3Nv4R
最後に
Firebaseにおいての上記の設計思想からModelを管理できるLibraryを作りました。
ライブラリの利用実績も増えて行ってます!ぜひ利用してください!!コントリビューターも募集しております!
MENTAでFirebaseを学ぶ講座やってます!
https://menta.work/plan/913
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についてさらに詳しく知りたい方は次をご覧ください。