2019年12月3日に開催された Firebase Meetup #15 で、 「CQRSを学ぶついでにCloud FunctionsとFirestoreを連動させる時の小技も学ぶ」というタイトルでLTをしてきました。
Firebase はGoogleが提供するmBaaSです。 Webアプリ・ネイティブアプリのバックエンドで共通して必要になる機能一式を提供してくれるサービスで、 うまく活用すると開発のコストを抑えながらスケーラブルなアプリケーションを作ることができます。 DeNAにおいても、実はさまざまなサービスでFirebaseが活用されています。
今回のLTでは、Firebase上にアプリケーションを構築する際の工夫として、 コマンドクエリ責務分離(CQRS)をキーワードにお話しさせていただきました。
スライドは公開しているのですが、それだけ見ても内容が伝わりにくいところがありますので、 この場を借りてフォローアップさせていただきたいと思います。
CQRSとは
リレーショナル・データベースにおける第三正規形(あるいはボイスコッド正規形)は、書き込み時にデータの整合性を担保することに重点を置いた設計であるといえます。 一方で、近年のアプリケーションにおいては『いかに高速かつ効率的にデータを読み取ることができるか』がより重要になってきており、 従来のベストプラクティスがそのまま通用しなくなってきています。 アプリケーションへのアクセスが増えるとこの傾向は顕著になり、少し複雑な要求に取り組もうとするともはや同一のモデルに対して読み書きすることが合理的ではなくなってしまいます。
そこで、サーバサイドの処理をデータに対する副作用があるコマンド(書き込み)と副作用がないクエリ(読み取り系)で明確に分けてしまおう、という設計思想がコマンドクエリ責務分離です。 一般的に、アプリケーションからのデータの読み書きは同一のデータモデルに対して行いますが、 CQRSを適用した世界では書き込みと読み取りでそれぞれ異なる要求に特化したデータモデル設計を取り入れることができます。
書き込み用と読み取り用でまったく異なるモデルを採用することも可能であり、設計の幅がかなり広がります。 こういったデータモデル設計は、( 永続データ構造 の実装である)イミュータブルデータモデルのような考え方とも非常に相性がよく、 堅牢かつ保守性の高いアプリケーションを構築する上で今後重要になってくるのではないかと考えています。
FirebaseでCQRSを採用する動機
書き込み用のモデルと読み取り用のモデルを分けるテクニックは、Firebaseの主要なデータストアであるFirestoreにおいても非常に重要です。
Firestoreではクライアント・アプリケーションからデータベースに直接アクセスすることが原則となっています。 読み書きの際には、 セキュリティルール という機能でユーザ認証・認可、データのバリデーションなど行います。
セキュリティルールは、読み取りオペレーションに関しては比較的シンプルに記述することができます。 一方で、書き込み系のルールはデータモデルの複雑度に比例して肥大化していきます。 データモデルのユースケースが増えれば増えるほど、ちゃんとしたセキュリティルールを書くことが難しくなります。
みなさんおなじみのTwitterを例に考えてみましょう。
Twitterの1件の ツイートを表すデータモデル は、 実は70個くらいのフィールドから構成されています。
このデータを読み取ることはそんなに難しくありません。 誰が見ても良い公開データですので、セキュリティルールもシンプルになります。 表示に必要となるデータが詰め込まれているため、クライアントアプリケーションでの取り回しもよさそうです。
一方で、このモデルの更新を安全に行うためのセキュリティルールは極めて複雑です。 たとえば、あるツイートに『いいね!』する処理は、このデータに関しては「いいね!数」をはじめとする一部分だけを更新します。 このとき、セキュリティルールでは『いいね!数』を増やすことを許可するだけでなく、 他の無関係なフィールドが書き換えられていないことも保証しなければなりません (「いいね!」が押されたときにツイートの本文を書き換わったりしては困りますね)。
Firestoreでは、データモデル設計は読み取りの要求に対して特化させることが有利です。 一方で、読み取りに特化したデータモデルは書き込み時の扱いが難しくなります。
コマンドクエリ責務分離を取り入れると、この課題の大部分を緩和することができます。 「いいね!」する処理は『どのユーザがどのツイートをいいね!したのか』という情報だけを持つイベントデータを作成することで表現します。 サーバサイドでは、 バックグラウンド関数 を利用して、 イベントデータの作成を契機に読み取り用のデータモデルを更新すればよいでしょう。
実装上のTips
という明らかに長い前置きを経て、ようやく本題です。
関数は細かく分ける
CQRSを採用すると、1つのイベントに対して複数のロジックを実行したくなることがよくあります。 先述のTwitterの例では、「いいね!」が押されたときに「いいね!数」を1増やすだけでなく、 そのツイートの投稿者に通知を送り、最近では「いいね!」したツイートをそのユーザのフォロワーのタイムラインに投下するなんてこともしていますね。
このように、1つのイベントに対して複数のロジックを実行する場合は、多少面倒でも関数を分けたほうがよいでしょう。 それぞれの関数がマイクロサービスのように疎結合かつ高凝集になり、メンテナビリティが向上します。 また、Cloud Functionsはリトライの可能性を見据えて処理を冪等に作ることが原則となっているので、 その観点でも小さな関数のほうが実現しやすいはずです。
一般に、1つの関数が2回以上Firestoreへの書き込みを実行していたら分割の可能性を検討してみるとよいと思います。
増えるFunctionsの整理
ところで、細かく分けると関数が大量に生成されることになります。 Functionsのディレクトリをどう整理するかはさまざまな流派があり、未だに結論が出ていません。
ここで紹介した実装を採用する場合は、 スライドの15ページ のようにFirestoreの構造と Functionsのsrcディレクトリの構造を一致させるとわかりやすいのではないかと思います。
呼び出し可能な関数は避ける
ここまで読んで「わざわざバックグラウンド関数でやる必要ないのでは?」と思う方もいるかも知れません。 Firebase界には呼び出し可能な関数という便利な存在がいて、たしかにこれを使っても同じような処理が実現できるかもしれません。
しかし、それはあくまで見かけの話です。 FirestoreとFunctionsの連携を考えたときに、呼び出し可能な関数の利用は最大の悪手です。 理由はたくさんありますが、ここでは一例を挙げるにとどめます。
- Firestoreの堅牢なネットワーク断への耐性とオフラインサポートが損なわれ、不要なエラーハンドリングを自分で実装することになります。
- 呼び出し可能な関数では、1回の呼び出しで全ての関連する処理を完結するのが原則となるため疎結合性が損なわれるか、結局バックグラウンド関数と併用することになりアーキテクチャ上の複雑度が増します。
- 呼び出し時の認証・認可やデータバリデーションなど、本来セキュリティルールに閉じ込めるべきロジックが分散し、同様に複雑度が増します。
というわけで、呼び出し可能な関数は本当に極稀に存在するどうしようもないユースケースで使う最後の手段です。
使ったらだいたい敗北です。
Updateトリガーの関数は避ける
バックグラウンド関数の中でも気の難しいやつがいます。 Firestoreのupdateでトリガーされる関数です。
updateでトリガーされる関数は、そのドキュメントがどのようなユースケースで更新されたのかに関わらず実行されます。 つまり、1つのドキュメントがさまざまな理由で更新されるアプリケーションでは、 関数の中でどの理由で更新されたのかを判別するロジックが必要になります。 このような処理は一般に煩雑かつ本質的ではない、できれば書きたくないコードです。
また、ユースケースの判別のためにドキュメントに状態を表すフィールドが必要になることも少なくありません。 (そして、それが新たな更新のユースケースを生むのです!) updateトリガーは呼び出し可能な関数ほどではないにせよ、使い所をよく見極めるようにしてください。
「似て非なるイベント」はコレクションを分ける
これはFirestoreのデータモデル設計において重要なポイントです。 Twitterの例で言えば「いいね!」と「リツイート」はなんとなく似たような処理をしそうなので、 同じコレクションにイベントタイプのようなフィールドをもたせて解決できそうな気がします。
しかし、これもupdateトリガーのバックグラウンド関数と同じ問題を発生させるため、避けるべきです。 イベントデータを格納するコレクションを複数のユースケースで使い回すのは得策ではありません。 横着をせずコレクションを分けたほうがよいでしょう。
CQRSもまた銀の弾丸ではない
さて、ここまでCQRSの良い面ばかりを述べてきましたが、他のあらゆる技術がそうであるようにFirebaseにおけるCQRSもまた銀の弾丸ではありません。
特にリアルタイム・リスナーを利用して読み取るコレクションをCQRSで更新する対象としたいケース(まさにツイートの例などが該当しますが)では、 レイテンシ補正が機能しなくなるなどの課題もあるため、総合的なユーザ体験を考慮してCQRSを適用するかどうかを判断しなければなりません。
Twitterの例をあげたところでセキュリティルールが複雑になることを課題として挙げましたが、 難しいけれども不可能ではありませんので、要求によっては従来のデータモデル設計で対応することも十分考えられます。
おわりに: どうしてこの記事が生まれたのか
まえがきではフォローアップとか偉そうなことをかいたのですが、(タイトルの長さを見たらわかるとおり)とても5分のLT枠に収まる内容ではなく、 当日も一番話したかった小ネタ部にはほとんど触れることが出来ませんでした。 (せっかくイベントに来ていただいた方、本当に申し訳ありませんでした…)
『枠に収まらないネタを持ち込むと不幸な結末を迎える』というのが今回の学びです。 この点に関しては大いに反省した上で、今後も最新の取り組みについてお伝えできればと思っています。
それでは、またどこかで。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。