概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Why Event Sourcing basically requires CQRS and Read Models | Arkency Blog
- 原文公開日: 2017/11/28
- 著者: Robert Pankowecki
- サイト: Arkency Blog
日本語タイトルは内容に即したものにしました。
用語
- CQRS: コマンドクエリ責務分離(Command Query Responsibility Segregation)
- CQRS Read Model: Query Modelとも呼ばれ、データの読み出しをデータの書き込みから分離します。CQRS Write Modelと対になります。
参考: The Read Model · Nick Chamberlain
Railsドメイン設計: イベントソーシングでCQRS Read Modelが基本的に必要な理由(翻訳)
イベントソーシング(Event Sourcing)は一定のメリットを得られる優れた技法ですが、1つ大きな制約があります。現在のステートの概念を簡単に得られないため、たとえば出荷可能数が10個以下の全製品が欲しいという問い合わせの回答を得るのが簡単ではありません。
ID=1
のProduct
としてイベントのProduct-1
ストリームを読み出し、それを用いてこの製品の現在のステートを再構成して、その製品の出荷可能数が10個より少ないかどうかという回答を得ることも一応可能ですが、すべての製品について回答を得るには、すべてのProduct-*
ストリームを列挙し、全製品の保存済みドメインイベントをすべて処理する必要があります。この操作には膨大な時間とコストがかかるでしょう。
普段の業務で目にする次のユースケースはいずれも困難が少し増すでしょう。
- 登録ユーザーの最新10人を表示する
- 顧客をメールか住所で検索する
- 今月からの全トランザクション数
- 顧客のライフタイムバリュー
- 「blue pillow」というテキストを含む全製品
他にいくらでも考えられます。
どうしてこうなってしまうのでしょうか。
その理由は、エンティティ(Entity)や集約(Aggregate)がイベントソーシングされると、オブジェクトのリポジトリへの問い合わせ方法が1つに限定されてしまうためです。その方法とはすなわちfind_by_id
です。
他の場所(他のエンティティなど)で得たこのid
が、その場所への参照(またはUIからの参照、APIからの参照、リクエストからの参照)を持つことは理解できるので、次のように書けます。
id = params[:id]
product = ProductRepository.find_by_id(id)
このリポジトリは、自分が読み取るべきイベントのストリーム(Product-1
など)を認識し、それらのイベントがProduct
のインスタンスに適用されることで1つの製品の現在のステートが再構成されます。以上。
では上述のユースケースをすべて解決する方法があるとしたらそれは何だかおわかりでしょうか?それがRead Modelです。
e-コマースアプリの製品リストを顧客に表示し、AddToBasket
やProduct
などのコマンドをイベントソーシングしたいのであれば、Product
のRead Modelが1つ必要になります。このRead Modelは個別の要件次第で、ElasticsearchやSQLなどどんなデータベースにも配置できます。
あるRead Modelを手順に沿って構成するにはどうしたらよいでしょうか。
- 製品の更新は、新しいドメインイベントを保存することで行う
- イベントハンドラがトリガされる
- イベントハンドラの保存後、イベントにプッシュしたメッセージキューでトリガ可能になる
- 最もシンプルなケースはActiveJobで実装可能、もっと複雑なシナリオではKafkaやRabbitやAmazon SQSで実装可能
- または別プロセス(プロジェクション)が保存済みドメインイベントを定期的にイテレーションして処理対象を選ぶ
- ドメインイベントの保存にEventStore DBを用いた場合は非常にシンプルに行える
- イベントハンドラは、発生するイベントと処理するドメインイベントの種類に応じてそのRead Modelを更新する
例を1つご紹介します。
ProductRegistered
イベントは、ProductList
というActiveRecordベースのRead Modelへの新しい要素の追加を発生させます。
ProductList.create!(
id: event.data[:product_id],
name: event.data[:name],
price: BigDecimal.new(event.data[:price]),
)
ProductPriceChanged
イベントは、リスト上の価格の更新を発生させます。
ProductList.
find_by!(id: event.data[:product_id]).
update_attributes!(
price: BigDecimal.new(event.data[:price]),
)
他にいくらでも考えられます。
そして価格の高い製品上位10種を表示したい場合は、このProductList
というRead Modelに基づいてアプリの読み出し側で行えばよいのです。
ProductList.order("price DESC").limit(10)
アプリの書き込み側では、イベントソーシングされているProduct
クラスの書き込みで変更をトラッキングしつつビジネスルールを保護します。書き込み側は、ProductRegistered
やProductPriceChanged
を公開する責務を持ちます。
お知らせ: もっと詳しく知りたい方へ
イベントハンドラやRead Modelやイベントソーシングについてもっと詳しく知りたい方は、私たちの近刊「Domain-Driven Rails ebook」をぜひ手にお取りください。