FirestoreとCQRSのメモ
概要
業務でFirestore + CQRSに触れる機会があったので、調べたことをメモとしてまとめます。
情報に誤りがあれば、ご指摘いただけますと幸いです。
CQRSって?
CQRSは、書き込み処理と読み込み処理で、データモデルを分けて設計しようという考え方です。
一般的には、アプリケーションからのデータの読み書きは同一のデータモデルに対して行います。
図でいうとこんな感じです。
この形式は基本的なCRUD操作を行う際に有効ですが、デメリットとして、アプリケーションの内容が複雑になると、それに合わせてモデルの設計内容も複雑になっていくケースが存在します。
たとえば、あるUserオブジェクトがあったとして、ユーザーの持つ権限の種類によって、それぞれのエンティティに対して書き込み/読み込み可能か否かが変わる仕様だったりするだけでも、結果的にUserオブジェクト内で判断する必要のあることはそれなりに多くなってしまいます。
そうして処理が複雑になると、モデル(オブジェクト)のパフォーマンスも下がりがちです。
一方で、CQRSに則ると、モデルを読み取り専用のもの、書き込み専用のものに分けることになります。図で表すとこんな感じです。
これにより、それぞれの役割に応じて最適化された内容のモデルを設計することができ、結果としてモデル内での処理の複雑さや、パフォーマンスの低下を防ぐことにつながります。
ちなみに図にある「連携・共有」というのは、書き込みモデルに書き込まれた内容を、読み込みモデルに反映させるための処理を指しています。
ただ役割によってモデルを分けただけだと、読み取り用のモデルの内容は一生正しい状態に更新されないので、その橋渡し的な処理を行う必要があるのです。
CQRSとイベントソーシング
イベントソーシングとは、データの現在の状態だけを保存するのではなく、「データが1件登録された」「データがhogehogeという内容で更新された」などのように、そのデータの変更に関する全てのできごと(イベント)を履歴として残しておく、というアーキテクチャです。
このアーキテクチャを利用することで、一体どのような良さがあるのでしょうか?
それは、どのタイミングのデータの状態についてもイベントをもとにして復元が可能だったり、さらにそこから広げて、データ更新まわりでバグが発生した際の調査に履歴情報を活用できたりする、ということなどがあります。
このイベントソーシングの考え方が、一般的にCQRSと相性が良いと言われており、よく併用して使われています。
具体的な相性の良さを説明すると、イベントソーシングを実現するためには、自ずと「イベント」と「現在のデータの状態」というデータに分類する必要が出てきます。
そことCQRSとを合わせて、「イベント -> 書き込み専用, 現在のデータの状態 -> 読み込み専用」として、仕組みの重ね合わせをしやすいために、これらのアーキテクチャの相性の良さが謳われています。
FirestoreでCQRSを採用するメリット
Firestoreで、CQRSおよびイベントソーシングを実現しようとする場合、たとえばこういう構造がおすすめです(参考: 実践Firestore)。
書き込み専用のコレクションはデータの変更に関するイベントだけを入れていて、イベントが入ったことを起点にしてCloud Functionsを発火させ、読み込み専用コレクションにデータの共有をする、といった流れです。
それでは、FirestoreとCQRSを組み合わせると、どのような良さがあるのかを整理していきましょう。
まずFirestoreの特徴として、読み取りと書き込みで特性が大きく異なるという点があります。
具体的には、Firestoreにはホワイトリスト方式のセキュリティルールが存在しますが、読み取りに関しては、対象のドキュメントの大きさが膨れ上がっていたとしても、ルールはそれに影響されず数行書けばdoneという感じです。
対して書き込みについては、対象のドキュメントが膨れ上がっていると、それに比例して書き込みを制御するルールの大きさも肥大化します。
なので普通にやると、たとえば10や20くらいのフィールドを持つドキュメントを新たにフロントから更新しようとするには、数十行のルールを別途加える必要があるわけです。
しかしCQRSかつイベントソーシングを採用すると、フロント側は上図のように、書き込み専用のコレクションにイベントだけ記録しにいけばよいので、ルールはその分だけの数行を加えるだけで済みます。
なので、この記事のタイトルに対する結論は、FirestoreをCQRS的に利用すると、フィールドが大量にあるドキュメントへの書き込みについて、セキュリティルールの大掛かりな変更をしなくて済むので楽、になります。
所感
CQRSはモデルの持つ役割を非常にシンプルにすることがわかりました。
とはいえ、たとえばFirestoreで考えると、単純に役割を分離した分だけコレクションが増えるわけなので、すべてのコレクションにいたずらに適用すると、Firestoreの構造が逆にややこしくなる可能性もありそうだなと思いました。
なので、コレクション・ドキュメントのデータ構造が複雑な箇所については、部分的にCQRSを適用する、くらいが現実解なのかなという所感です。
参考記事
Discussion