Edited at

Cloud Firestoreを実践投入するにあたって考えたこと


はじめに

Firebase Realtime DBを実践投入するにあたって考えたことを読んで頂いてありがとうございます。 多くの方から「いいね」を頂いて、今回のこの記事を書くモチベーションになりました:bow_tone1:

本当にありがとうございました!

さて、CloudFirestoreは、Firebase Realtime Databaseとは全く違うデータベースです。特にSubCollectionQueryが導入されたことにより、リレーションシップの設計に関して大きく異なります。

この記事では、主に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

このあたりの説明は丁寧にドキュメントで説明されているのでこちらをご覧ください。

一般的な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の複数の方法でリレーションシップ作ることが可能です。まずはその種類を紹介します。タイプ別に種類を図にしました。

スクリーンショット 2018-11-30 13.06.56.png

最終的にこの8パターン組み合わせになるのかなと考えています。

以降Swiftのコードが掲載されますなんとなく読めると思うのでご参考ください。


■ Key

スクリーンショット 2018-11-30 13.19.34.png

これはRDBでも使われる一般的なリレーション方法です。RDBで言うならテーブルに参照先のレコードのIDを持っている状態です。ここでは、ItemuserIDを保持していることからItemUserの関係を表しています。


■ Reference

スクリーンショット 2018-11-30 13.38.53.png

これは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
}

バージョンの変更に合わせて、データをマイグレーションできます。増大したデータのマイグレーションにはコストもかかるので、モデルのバージョンをあげることは稀ですが可能です。

しかし、ここでリレーションに話を戻すと問題が出てきます。

ItemUserの関係を表すuserIDはIDのみを保持しており、バージョン情報を持っていません。そこで登場するのがReferenceになります。Referenceパスそのものを保持することが出来るようになります。


Referenceは多用できない

「Reference便利💪🏻」となったかも知れませんが、Referenceは多用できません。なぜでしょうか?ItemにReferenceを持たせるとどうなるかを考えてみましょう。

次の状態では、Itemはバージョン1Userを参照しています。

// /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

スクリーンショット 2018-11-30 14.37.58.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を利用してシンプルにセキュアな情報を保持できます。


Same IDを使ったソーシャル機能

/version/1/user/:user_id

/version/1/social/:user_id

UserSocialを別のCollectionに定義しています。

Userに定義されるであろうデータ

{

"name": "1amageek",
"age": 31,
"gender": "male"
}

Socialに定義されるであろうデータ

{

"followerCount": 2000,
"followeeCount": 23
}

一見全てユーザーがもつべきデータに見えますが、UserSocialには明確に分けるべき理由があります。



  • Userデータは自分以外から更新させたくない


  • Socialデータは自分以外からの更新できるようにしたい

理由はそれだけではありません。Socialデータの方が圧倒的に更新頻度が多いはずです。

ここでUserデータの特性を考えてみましょう。このデータはどこで利用されるでしょうか。ソーシャル機能を作っているのであればユーザー検索は必須な機能となり得るでしょう。Cloud Firestoreの検索機能は非常に貧弱なのでAlgoliaやElasticSearchなどに検索機能を任せることが考えられます。

同時にAlgoliaやElasticSearchの更新にはCloud Functionsを利用することが想像できると思います。もしUserデータにSocialデータを含めていたらどうなるでしょうか。カウントがインクリメントされるだけでCloud Functionsがトリガーされることになりそうです。


■ Query

スクリーンショット 2018-11-30 16.03.45.png

このリレーションの方法もRDBでも使われる一般的な方法です。RTDBとは違いwhereが利用できるようになってとても便利になりました。


■ Sub Collection

スクリーンショット 2018-11-30 15.48.50.png

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

スクリーンショット 2018-11-30 16.56.09.png

この方法もRDBで使われる中間テーブルをもつ方法です。


■ Reference Collection

スクリーンショット 2018-11-30 17.05.23.png

この方法は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 AUser Bfollowersとして保持し、User BUser Afollowersとして保持する。

FirestoreではWriteBatchを使って複数の書き込み先に同時に一度に書き込むことが可能なので、どの処理も簡単に行えます。

トランザクションと一括書き込み

Firestotre のバッチ処理とトランザクション処理


■ Duplicated Collection

スクリーンショット 2018-11-30 17.54.05.png

この方法は、セキュリティを保ちつつデータを参照するデータを参照する方法です。

決済情報をもつデータ構成を考えてみましょう。


ShopProductUserが購入した情報をTransactionとして保持する。

TransactionShopUserのみが参照できる。


この要件を満足したい時、Transactionはどこに保持するのがいいでしょうか。

まず、Transactionには必ずセキュリティルールを設定するので、横断的なQueryは機能しなくなります、そのためルートコレクションに配置することは避けた方が良さそうです。

次に、セキュリティを担保するためにはSubCollection構造にするのが良さそうですが、ShopUserのどちらに持たせるのがいいでしょうか?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に持たせる事も可能です。

例えば下の図のように強制アップデートが必要なバージョンやアプリが利用可能かを示すフラグを持たせることでハンドリングしましょう。

スクリーンショット 2018-12-01 9.56.58.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!


■ Model内にパーミッションを持たせる

public privateなどのパーミッションを持たせることで、セキュリティルールのハンドリング簡単に行うことが可能になります。

スクリーンショット 2018-12-01 10.21.17.png

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についてさらに詳しく知りたい方は次をご覧ください。