2016年12月5日
CQRSとイベントソーシングの使用法、または「CRUDに何か問題でも?」
本記事は、原著者の許諾のもとに翻訳・掲載しております。
プログラミングの世界はますます関数型の方向に向かいつつあります。Haskellみたいな純粋関数型プログラミング言語はまだ主流にはなっていませんが、高階関数やイミュータブルなデータなどの考えかたは、Java 8やECMA Script 2015などの言語にも ラムダ関数 として導入されています。
アーキテクチャレベルでは、 CQRS や イベントソーシング が台頭してきました。これらは、リレーショナルデータベースやNoSQLデータベースをバックに持つ昔ながらのCRUDデータモデルの代案となるものです。なぜ関数型に向かいつつあるのかを知るために、まずは昔ながらのアーキテクチャについて振り返ってみましょう。単一のデータストアを用いた N層アーキテクチャ です。
昔ながらのN層アーキテクチャとCRUD
N層アーキテクチャやその変形版は、データを扱うアプリケーションで人気のあるパターンです。ローカルのデスクトップアプリケーションからオンラインのSaaSソリューションまで、あらゆるところで使われています。その考えかたはシンプルです。関心の分離によって、プレゼンテーションとビジネスロジックと永続データをそれぞれ切り離します。データ層は、データストアの読み書きを担当します。他の層は、理論上はデータの永続化メカニズムを気にせずに済むようになります。データ層はドメインモデルを公開するでしょう。ドメインモデルとは、そのシステムが扱うエンティティを、データ型や関連(ひとりの「ユーザー」を複数の「アカウント」と紐づけることができて、単一の「個人設定」インスタンスを持つなど)などの観点で表したものです。データの変更は、 CRUD (Create、Read、Update、Delete)のC、U、Dで表されます。プログラミングプラットフォームの中には、これらの操作に必要なコードを自動生成する仕組みを持つものもあります( Ruby on Rails や .NET の例をごらんください)。
さて、これに何か問題があるのでしょうか?このモデルは一般的に広く使われており、多くの人は他のモデルについて考えることすらありませんでした。そして、シンプルなアプリケーションならこのモデルで何の問題もなかったのです。しかし、少し凝ったことをやりはじめると、エンティティ全体を丸ごと更新するのではうまくいかない例が出てきがちです。Userオブジェクトには、IDや作成時刻などの自動生成されるデータが含まれるかもしれません。これらはエンティティを読み込むときには見えますが、データ層自身による更新は許可されていないでしょう。また、ユーザーによって権限が異なり、エンティティの一部だけを更新できるユーザーもいれば一部だけしか見えないユーザーもいるかもしれません。単なるCRUDではなくもっと細かい操作をデータ層で公開したくなるかもしれません。たとえばドメインオブジェクト全体ではなく一部だけを操作するようにして、特定の環境で特定のフィールドを書き換えられないようにするといったものです。あるいは、一部のフィールドをそもそも無視してしまいたいこともあるかもしれません。
データの読み込みと書き込みは別のもの
実際のところ、データの書き込みと読み込みとでは、検討すべきことが大きく異なります。
書き込み | 読み込み |
データの整合性の維持 | データの検索と抽出の効率化 |
アトミックな更新/トランザクション | 導出値(合計など)の算出 |
バージョン管理(楽観的並行性制御/楽観的ロック) | 複数のビューの提供 |
書き込み権限の管理 | 行レベル、カラムレベルの権限管理 |
書き込みと読み込みのどちらに力を入れているかは、ストレージエンジンによって異なります。たとえば昔ながらのリレーショナルデータベースは、外部キーなどの制約を使ってデータの整合性をうまく制御できるようになっています。一方でNoSQLデータベースは、スループットとスケーラビリティを確保するために、そういった組み込みのガードレールをはずしてしまいました。データ層においても、どちらか一方に特化した最適化をすることがあります。たとえば、あらかじめ計算済みの値を保持しておけば、「一日あたりのサイト訪問者数」などの読み込み操作を効率よく行えるでしょう。ストレージソリューションのメーカーはどこも、「うちのプロダクトならあらゆるニーズを満たせます」などと自社製品の機能を自慢します。しかし実は、昔ながらのCRUDモデルに沿ってストレージエンジンを選んでデータ層を設計した時点で、さまざまな関心事の間で何らかの妥協をしていることになるのです。
CQRSへようこそ
暗い未来しか見えませんが、何らかの代替案があるのではというヒントにもなります。ここで登場するのがCQRS(Command Query Responsibility Segregation:コマンドクエリ責務分離)です。CRUDと違ってCQRSは、データの読み込みと書き込みは違うものだという前提にもとづく考えかたです。CQRSでは、データベースの操作をコマンド(データを書き換える操作)とクエリ(データを読み込む操作)の二つに分類します。コマンドは一般に、操作の成否以上の情報を呼び出し側に返しません。また、クエリは冪等であることが保証されます。間にコマンドが挟まらないという前提で、同じクエリを何度実行しても結果は同じになるということです。RESTでいうと、コマンドはPUTやPOSTに対応し、クエリはGETに対応するものです。
CQRSを深く考えずに実装するなら、単純にcreate、update、deleteのコマンドを用意すればよさそうです。しかしこれは、大事なことを見落としています。読み込みに使うデータモデルとコマンドを「別のもの」として明示的に切り離すということは、データを問い合わせる際に使うUserモデルとコマンドを実行するときに使うモデルとが違っていてもかまわないということです。ユーザー情報を更新するというのではなく、「メールアドレスを変更する」「請求先情報を変更する」なとどいうコマンドを考えることができるのです。CQRSなら、エンティティのどのフィールドが更新可能なのかといった混乱はなくなります。コマンドには、そのコマンドに関連するフィールドだけを含めることになるからです。パーミッションの考えかたも簡単になります。呼び出し元が変更しようとしているエンティティのフィールドが本当に変更してよいかどうかをチェックするのではなく、呼び出し元に特定のコマンドを実行する権限があるかどうかだけをチェックすればいいのです。
複数のデータストアを使ってCQRSを実装することもできるし、コマンドやクエリのファサードを用意して詳細を隠すこともできます。また、コマンド用とクエリ用にそれぞれ特化したデータモデルを組み合わせて使うこともできるでしょう。
しかしここで、私たちはどうやらふりだしに戻ってしまったようです。コマンドに適したデータモデルとクエリの結果を表現するためのデータモデルは手に入りましたが、それらの間でデータを移し替える手段がありません。いま欲しいのは、単一のソースを元に、データの整合性を維持しながら複数の表現方法を使える仕組みです。CQRSというケーキを手に入れたので、それをおいしくいただきたいところですね。
イベントソーシングの導入
そこでイベントソーシングですよ。イベントソーシングはCQRSとの相性が非常によいものですが、それ単体でも成立するパターンであり、私たちがふだん使っている多くのソフトウェアの裏側で採用されています。たとえばファイルシステムやデータベースエンジンなど(あ、こちらはそんなにびっくりすることでもないかもしれませんね)でも使われているのです。イベントソーシングの動きを理解するには、会計処理と比較してみるとわかりやすいでしょう。会計処理では、入出金取引(売上や購入など)を元帳にだけ追記していくのががよいとされています。新しい取引が発生すると、元帳に新しいエントリが追加されます。そして現在の口座残高は、必要に応じて過去のすべての取引から算出します。CRUDモデルでは、取引が発生するたびに残高を増やしたり減らしたりしていました。これと比べると、イベントソーシングの利点が見えてくるのではないでしょうか。
イベントは、CQRSのコマンドと関連づけられるものです。ただ、コマンドは何らかの意図や要望(却下される可能性もあるもの)を表しますが、イベントは既に発生した事実を表します。ユーザーのメールアドレスを更新するコマンドが実行されてから、「メールアドレスが更新された」などというイベントが発生するのです。しかし、コマンドとイベントの関連は、この例ほどには明白にならないこともあります。たとえば、ユーザーの住所を変更するコマンドを実行したときに発生するイベントには「住所の緯度経度情報が変更された」などというものが含まれるかもしれません。これはコマンドそのものの中には含まれていなかった情報です。
イベントストリームを扱えるように最適化されたストレージエンジンがあれば、私たちが欲していたパズルの一片を補ってくれるでしょう。そんなストレージエンジンのひとつが EventStore です。EventStoreは追記限定の不変なストリームとしてイベントを扱います。また、メッセージバスとしても機能して、システム内の他のサービスからの新しいイベント対してまるで永続化されているかのようにリアルタイムで応答できます。この手のプロダクトを使えば、イベントを元にしてクエリモデルがストレージを更新できるようになるでしょう。これは、共有データストアを用いたCQRSモデルにおけるデータベースの更新とほぼ同じですが、大きな違いがひとつあります。イベントが使われるので(イベントは既に発生した事実を表すのでしたよね)、クエリモデルはもはや、パーミッションやバリデーションやデータ整合性などを気にかけずに済むようになるのです。必要に応じた最適な形式でデータを格納できるようになるし、後で使うかもしれない値を事前に計算しておくこともできるでしょう。データベースだけではなく全文検索インデックスも更新しなければいけないですって?問題ありませんね!ふたつのエンティティを更新しなければいけない(ひとつは管理者用でひとつは一般公開用に情報を絞ったもの)ですって?どうぞどうぞご自由に。
実際、ひとつのエンティティに対して複数のビューを持てるなら、さらに一歩先に進めるようになるでしょう。もはや、あらゆる更新に対応できるような単一のクエリモデルを維持する必要はありません。「ユーザーの詳細情報の表示」「一日あたりのアクティブユーザー数のレポート出力」など、用途にあわせて最適化したクエリモデルをいくつでも作れるのです。発生した事実を表す唯一のソースはイベントストリームであり、クエリモデルは単に、必要に応じていくらでも作り直せる便利な表現手段に過ぎなくなります。
イベントソーシングのその他の利点
イベントソーシングは、クエリデータモデルとコマンドデータモデルを同期させる仕組みとしてすばらしいものです。しかしそれ以外にも、間接的に得られる利点がたくさんあります。
- 実証可能な監査証跡を残せる: あらゆる変更がイベントとして永続化されるので、システムが扱うすべてのデータについて、その変更履歴を追えるようになります。イベントにはアノテーションを付与できるので、その変更を誰が実行したのかといった情報も記録できるし、たとえばコマンドの実行元のネットワークアドレスなども記録できます。昔ながらのCRUDアーキテクチャでも、そういった監査情報をデータとして残すことはできます。しかし、データベースに記録された変更履歴がほんとうにデータストアの変更と一致しているかどうかを証明するのは困難です。
- 履歴データに基づいたクエリモデルを作れる: イベントは、エンティティのこれまでの完全な動きを示すものです。単に現状だけを表すものではありません。当初は想定すらしていなかったようなレポートやクエリモデルが必要になったとしても、対応できるでしょう。CRUDモデルのエンティティには「最終更新時刻」が記録されていることがよくありますが、イベントソーシングのモデルであれば、過去のすべての変更について、その時刻を取得できます。それだけではなく、任意の日時にさかのぼってその時点のエンティティの状態を再現できるのです。
- 副作用を気にせずに済む: イベントを発行するコマンドサービスを、その変更に伴って動くように作られているロジックと切り離すことができます。メールアドレスを登録したユーザーに対して「ようこそ」メールを送信したければ、「メールアドレス更新」イベントを受けたときにメールを送信する処理を書けばいいのです。メールアドレスを登録するロジックに手を加える必要はありません。
- トラブルシューティングやデバッグの助けになる: 運用環境で発生したバグを再現させたいですって?運用環境で起こったすべてのアクションをわざわざ実行しなおさなくても、同じ状態を再現できます。発生したイベントをコピーしてリプレイすれば、システムの動きを正確に確認できるのです。同じく、自動テストのときにも実際のイベントログを使えるでしょう。何かの変更を加えたときに、その結果が期待どおりであることを証明できます。
Elder Sourcerer – CQRSとイベントソーシングを実装するためのフレームワーク
Elder ではここで紹介したようなアーキテクチャを採用しており、そのフレームワークである Sourcerer をMITライセンスで公開しました。Sourcererは、Java 8でCQRSとイベントソーシングを実装するために必要な構成要素を提供します。そのほかに、力仕事の大半もこなしてくれるでしょう。 EventStore との組み合わせで使いますが、特定のストレージに依存しないように作られています。詳細は Elder Open Source SoftwareのGitHub をごらんください。連絡先はtech at elder dot orgです。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa