Share via



July 2016

Volume 31 Number 7

CQRS - CQRS を利用した応答性の高いシステムの作成

Peter Vogel | July 2016

コマンド クエリ責務分離 (CQRS: Command Query Responsibility Segregation) パターンは、この 3 ~ 4 年で普及が進んでいます。確かに、複数のプロセスから更新される一連のデータがあるコラボレーション シナリオには、CQRS が欠かせないツールです (Dino Esposito が 2015 年 6 月の Cutting Edge コラム「一般的なアプリケーション向けの CQRS」(bit.ly/1OtQba3) で CQRS の使用についてより広範に説明しています)。今回は、さらに詳しく、はっきりと説明します。実際、CQRS は ASP.NET MVC 開発者にとって既定の設計パターンで、データをクエリしてビューに表示し、そのデータが MVC コントローラーにポストバックされるときにコマンドを発行してテーブルを更新します。

しかし、CQRS は、大規模戦略の一部として用いるべき戦術です。この戦略の最初の手順が、ドメイン駆動設計 (DDD) です。これについては、2013 年 6 月の Julie Lerman のコラム「DDD 境界コンテキストを使って EF モデルを縮小する」(bit.ly/1TfF7dk、英語) で説明されています。DDD では、連携して動作する複数のドメインにアプリケーションを分割します。もちろん、各ドメインには専用のビジネス モデルや、独自のデータベースが存在してもかまいません。DDD には、ドメインをそれぞれに独立して開発しながらも、ドメインを連携させることができる戦略と戦術が用意されています。

在庫ドメインの定義

しかし、DDD を使用しても、連携の必要がなくなる可能性があるだけです。たとえば、オンライン販売アプリケーションを考えてみます。ここで重要な共有データは、在庫レベルです。在庫がないものを販売しないように、企業は各最小在庫管理単位 (SKU) の手持ち在庫数量 (QoH) の正確な数を把握するか、常に「念のための」在庫を余分に用意しておきます。現在のようにゆとりのない世界では、2 つ目のオプションはあまり検討されません。 企業は、常に必要以上の在庫は持ちたくないと考えています。

ビジネス取引に応じた QoH の更新は、想定よりも複雑です。実際の在庫システムでは、さまざまな取引を処理しています。明確な取引では、当然、SKU の販売時に QoH を減らし、新しい SKU の入荷時に QoH を増やします。さらに、SKU ごとに QoH の実状を判断するため、定期的に「棚卸し」を行います。適切に管理されたシステムでも、在庫の精度は 100% にはなりません。そのため、棚卸しによって在庫レベルの調整が必要になります。また、SKU になんらかの欠陥が見つかって、SKU を在庫から除くこともあります。SKU が販売されて在庫から除かれた後、顧客が注文をキャンセルして、SKU が棚に戻されることもあります。

企業は、このようなさまざまな取引をすべて追跡して、各取引に固有の情報を管理しなければなりません。たとえば、新しい SKU が入荷した場合、企業は SKU の購入に使用された請求書を把握する必要があります。SKU に欠陥が見つかった場合は、その理由を認識する必要があります。棚卸し中は、在庫の食い違いの程度を知る必要があります。経理部門は、「企業状態」を正確に報告するためにこうした情報が必要です。業務部門は、将来の計画を正確に立てるためにこうした情報が必要です。この追加情報にはこのようなニーズがあるため、取引を単なる在庫の増加や減少として扱うことはできません。

しかし、こうした取引はすべてが同じドメインに属しているわけではありません。取引は、「販売」、「経理」、「業務」、「入荷」といったドメインに分割されます。取引をドメインに分割するのは、ドメインごとにニーズが異なる現実を反映しています。

たとえば、ほとんどのドメインは最新データを必要としません。それらのドメインが実際の取引より 1 営業日遅れて処理を行っている場合、データが最新でなくても問題にはなりません。たとえば、経理部門は、月末の在庫に関する財務状態のみを知る必要があります。しかも、翌月になっても数日程度は許容範囲です。在庫データを最新に近い状態に保つことはできますが、ビジネス上そうしなければならない理由を見つけるのは難しいでしょう。このような部門の在庫情報は、「最終的に矛盾しない」状態であればかまいません。

しかし、販売システムでは、「最終的に矛盾しない」在庫情報では話になりません。販売部門は、SKU を顧客に示せるかどうかを判断するために、その時点の QoH を把握できなければなりません (「在庫が 2 つしかありません。 今すぐ注文してください」)。実のところ、SKU の QoH に関して必要な数値は、ほとんどのドメインで 1 つですが、販売システムでは QoH を 2 つの数値として管理します。1 つは「予約済み」数量 (注文を行っているユーザーが要求している SKU)、もう 1 つは「引き続き販売可能」な数量です。 顧客が 2 つ商品を購入すると予約済み数量が 2 つ増え、販売可能数量が 2 つ減ります。販売プロセスの終わると、予約済み数量が 2 つ減ります。ユーザーが注文をキャンセルすると販売可能数量が 2 つ追加されて元に戻ります。

経理部門と業務部門はどちらも、さまざまな方法でテーブルを結合できる柔軟性をリレーショナル データベースに求めています。また、特定の問題を発見する前に検討していなかった方法でそのデータを検索できる機能も求めています。関連するデータの量や、取引履歴調査の必要性を考えると、ページングも必要になります。

販売システムには柔軟性はあまり求められません。エンティティ間の関係は、検索要件と同様に、UI の設計によって固定されます (ただし、ページングのサポートはやはり必要です)。

応答時間のニーズもドメインによって異なります。多くの部門では、秒単位の応答時間でも仕事に悪影響はありません。しかし、販売システムでは、応答時間を 1 秒よりも短い単位で測定する必要があります。

これらのニーズすべてを満たす 1 つのシステムを構築するのは困難です (おそらく不可能です)。ドメインごとにアプリケーションを構築するのは、少なくとも可能です。たとえば、製品管理ドメインには、新製品と既存製品の情報の両方を使用して絶えず更新される製品一覧があります。一方、販売ドメインには、製品管理ドメインのデータと定期的に同期される読み取り専用/クエリ専用の製品一覧があります。

ドメインにとっては、単一責任の原則が適用されるのは企業レベルだと考えられます。各ドメインは、企業の 1 部門に適切に対処します。企業は複雑ですが、各ドメインは (比較的) 単純にできます。

CQRS ソリューション

しかしながら、これらのドメインは依然、在庫レベルを共有します。取引が経理や入荷などのドメインを経ていくときに、販売システムに在庫レベルの変化を通知する必要があります。販売システム内でも、複数の顧客が同じ SKU を購入しようと、それぞれが在庫レベルを上下させるので、在庫数の調整に合わせ、ある程度のロックを必要とすることがあります。

このような場合に、一般の ASP.NET MVC 開発者が考える以上のことに対応する CQRS パターンが役立ちます。たとえば、多くのドメインでは、アプリケーションから独自のデータベースにクエリできます。このデータベースには、ドメインに必要な情報が含まれています。在庫レベルを調整するコマンドを発行するときは、すべてのドメインがオンライン販売ドメインのデータを更新する必要があります。義務はどちらにもあります。 商品が販売されると、販売システムでは経理や業務などのドメインに各 SKU の販売による QoH の変化について通知する必要があります。

しかし、各ドメインは、別のドメインのデータを更新するのではなく、別のドメインに関連する情報 (この場合は QoH) を別のドメインに通知する義務を負うだけです。各ドメインは、そのドメイン自身のデータの管理方法を把握していますが、別のドメインのデータについては知りません。そのため、各ドメインが、自身のデータを更新する責任を負う必要があります。

たとえば、業務ドメインは絶えずデータ間の関係を調査して、在庫のニーズを予測したり、在庫レベルの変化を引き起こす要因を判断します。このドメインでは、従来のリレーショナル データベースが提供するデータ クエリの柔軟性をサポートする必要があります。業務ドメインの複雑さは、そのドメインに必要な分析の種類に起因します。

一方、販売ドメインが必要とするのは、もっとシンプルなものです。販売ドメインでは、SKU の QoH (予約済みと販売可能) を把握しておく必要があります。販売システムでは、SKU ごとの ID すべてとそれぞれの 2 つの QoH の数値を記憶しておくだけです。在庫品目の数が多くて記憶できない場合は、在庫の 20% を記憶しておき、残りの 80% を企業の販売活動を進めてもよいでしょう。たとえば、業務ドメインが柔軟性を提供しなくてもよい在庫品目は、販売取引をサポートするように設計されたなんらかの NoSQL データベースに保持できます。販売ドメインの複雑さは、応答時間を抑える必要性から生じます。

このような違いがあることから、業務ドメインが販売ドメインの QoH の数値を更新する方法を把握することは期待できません (もちろん、その逆も同様です)。

したがって、各ドメインはおそらく (そのドメイン自体の) 1 つのデータベースにはクエリしますが、別のデータベースにはコマンドを送信することになります (たとえば、販売ドメインには QoH の更新を送信します)。DDD はさまざまなビジネス要件を持つドメインを分離する戦略を提供しますが、CQRS はそれらのドメイン間の更新を管理する戦術の 1 つを提供します (CQRS のクエリ面についての詳細な説明は、2016 年 3 月の Dino Esposito のコラム「CQRS アーキテクチャのクエリ スタック」(bit.ly/1WzjvPi) を参照してください)。

コマンドとイベントの扱い

もちろん、取引ごとに通知する煩わしさ解決しようとして、ドメインのアプリケーションを複雑にすることは本意ではありません。更新が必要なドメインすべてを追跡するのではなく、さまざまなドメインへの通知を担うユーティリティ (通称、「コマンド バス」) に取引を各アプリケーションから送信します。新しいドメインを定義する (または既存のドメインがニーズを変更する) と、取引を生成するドメイン内のコマンド バスのみが、必要な新しい通知を反映して更新される必要があります。

これらの取引は、コマンドとイベントという 2 つのカテゴリに分離できます。この 2 つの区別は、技術的というよりも、概念的なものです。コマンドとイベントは、実際にはどちらも取引に関する重要な情報をまとめたメッセージです。在庫取引の場合、これは、SKU の ID、在庫レベルの実際の変化、および取引に必要な追加データです (商品の入荷ならば、ベンダー番号や請求書番号、棚卸しならば、SKU を実際に数えた従業員の ID などになります)。これらのメッセージは、POCO オブジェクトまたは XML/JSON ドキュメント (または、ドメイン間でのデータ送信方法によってはその両方) としてエンコードされます。

ここでのコマンドの定義は、「タスクを実行するために単一の受信者に送られるもの」です。コマンドは、普通、即座に実行する必要があるタスクで、当然、タスクの実行前に送信されます。また、コマンドは成功/失敗の応答を返すことも想定されます。アプリケーションはこの情報を使用して、すべてがうまくいったかどうかをユーザーに通知します (または、これを受けてアプリケーションはクエリを実行し、達成した結果を示すデータを取得することもあります)。取引を発生させたドメイン内での更新の大半は、おそらくコマンドで処理されます。

これに対してイベントは、タスクの実行後に発生し、複数の受信者が処理できます。また、通常即座に処理することは求められません。イベントは (少なくとも即座には) 結果を返すことを期待されません。イベントで問題が発生した場合、アプリケーションは通常、遅れて返されるメッセージによって問題を確認します (「申し訳ございません。クレジット カードが拒否されたため、注文を処理できません」)。取引を発生させたドメイン以外の更新の大半 (すべてではない) は、おそらくイベントで処理されます。

また、多くの概念的な区別と同様、これはおそらく連続体です。「明らかに」コマンドであるメッセージも、「明らかに」イベントであるメッセージもありますが、受け取り方によっては意見が分かれるメッセージもあります。

1 つのドメインの 1 つの取引によって、コマンドとイベントの組み合わせが生成されることがあります。商品搬入口に新しい SKU が到着したとします。SKU が適切に入荷されると、そのドメインのバスから販売システムにコマンドが送信され、その SKU の QoH が即座に増加します。バスでは、「何かが発生」し、月末に考慮すべきことが経理システムと業務システムに通知されるように、イベントもポストします。関連するメッセージを見ても、おそらく、メッセージの名前を見る以外、どれがイベントで、どれがコマンドなのか判別するのが難しい可能性があります。イベントには過去形の名前 (GoodsReceived)、コマンドには命令形の名前 (IncreaseInventory) が付けられる傾向はあります。

バスでは、そのドメインで即座に実行するために RESTful サービスを呼び出して販売システムにコマンドを送信する場合があります。イベントは、別のドメインで都合のよいタイミングで実行されるよう、メッセージ キューに書き込まれる場合があります (このオプションのいくつかについては、VisualStudioMagazine.com (英語) の記事「ドメイン イベントで最終的な一貫性を実装してアプリケーションを簡略化する」(bit.ly/1qn1wwV、英語) で説明しています)。

もちろん、Web サービスに送信するコマンドを使用したとしても、Web サービスの背後で行われる処理はだれにもわかりません。 同時に行われる大量の要求を処理するために、ドメインの Web サービスでは、コマンド メッセージをキューに書き込んで「ありがとうございます、承りました」と返すだけにして、応答時間を短くし、スケーラビリティを向上しているかもしれません。コマンドをキューに書き込むと、スケーラビリティの向上に加えて、致命的になり得る問題からドメインを回復できるようになります。たとえば、データベースやネットワークがダウンした場合、販売システムはサービスが回復するのをひたすら待機してから、キューに格納されたコマンドを処理できます。そのため、コマンドでもキューに含めることができます。

前に説明したように、コマンドとイベントの区別は概念的なものであり、技術的なものではありません。

コマンドとイベントの処理

CQRS のおかげで、アプリケーションは 2 つのデータベースの組み合わせを扱えるようになります。1 つはクエリ用データベース (おそらくはドメインに対してローカル) で、もう 1 つはコマンドとイベントのターゲットになるデータベースです。たとえば、販売システムは、QoH データを保持する NoSQL データベースを含むデータ ストアと連携し、業務アプリケーションと経理アプリケーションは、イベント/コマンドの履歴を基に体系化されたデータ ストアと連携します。

2 つのシステムの違いは、販売システムが応答時間のニーズを満たすために現在の在庫レベルの状態のスナップショットを必要とするのに対し、業務ドメインと経理ドメインでは分析をサポートするために各 SKU に対する処理の履歴を必要とすることです。業務ドメインと経理ドメインは、イベント ソーシングという別の戦術を使用して対処できます。イベント ソーシングを使用すると、通知されたイベントの監査ログがドメイン ロジックによって処理されて、最終的な応答が提供されます (経理システムの場合、「これまでの取引履歴によればと、在庫の現在価値は X ドルです」など)。

イベント ソーシングには長所と短所があります。イベント ソーシングでは、取引リストを再処理することで、常にデータの現在状態のスナップショットを作り直すことができます。この機能はイベントの一覧に調整を加えることができるため、経理では重宝されます。イベント ソーシングで、潜在的なイベント (予期される受け渡しと販売) を処理して、将来像を示すことも可能です。この機能は、計画を立てるときに業務で重宝されます。

しかし、イベントの一覧が増加すると、応答時間も遅くなります。経理では、「最後の既知の適切な状態」(おそらくは先月末の決算の数値) からロールフォワードして、月末の数字を表すスナップショットを生成できます。このスナップショットは現在の「最後の既知の適切な状態」として保存され、月末レポートとして公開されます。業務では、当日から将来の不定の時点までロールフォワードできます。おそらくはスナップショットは生成せず、要求されたら毎回将来のスナップショットを再作成します。これらのドメインに対する応答時間の想定を考えれば、これらは妥当なシナリオだと思われます。

しかし、イベント ソーシングを使用して販売システムの現在の QoH を判断するには、販売システムでは前回の棚卸し以降の取引すべてをロールフォワードする必要があります。棚卸しには労力がかかるため、棚卸しが頻繁に行われることはありません。結果として、最後の棚卸し以降のこれらのイベントをすべて処理すると、販売システムにとって許容できない応答時間が発生します。代わりに、販売システムでは、絶えず更新される QoH の数値を記憶します。

まとめ

クエリではデータベースでのさまざまなレベルのサポート (また、その結果としてさまざまなインデックスや外部キー/主キー) が必要ですが、更新はそうではありません。事実、すべての更新は、関連するエンティティの ID に基づきます。たとえば、在庫 SKU の一覧と QoH の数値は、すべて SKU ID に基づきます。これによって、CQRS システムのコマンド側のデータ モデルを劇的に簡略化できます。SalesOrder に対して SalesOrderItems のコレクションを生成する Entity Framework の機能は、コマンド/イベント メッセージに取引で変更された SalesOrderItems のすべての ID が含まれる場合には関係ありません。

この設計の結果として、データベースのロックに影響するのは興味深い部分です。販売システムの QoH と予約済み数量に対する更新は、1 つまたは両方の整数値の変更で構成されます。つまり、最低限のロックが必要です。それらのシステムでイベント ソーシングを使用している場合、他のいくつかのシステムのロックが解消されます。取引では、常にいくつかの取引をイベント テーブルに挿入するため、更新はありません。

事実上、企業には、複数の独立した処理機構があり、ドメイン内のデータを更新したり、コマンドやイベントを処理しています。そのため、ロックなしでは、競合が発生する可能性があります。たとえば、2 つの商品を購入するコマンドは、QoH を 0 に減らすイベント (だれかが棚卸しをして棚に何もないことに気付いた) と同時に現れる場合があります。面白いことに、キューベースのイベントソーシング アプローチを使用すると、この問題が解決されることがあります。販売システムの QoH 更新処理機構は、イベントソーシング ベースで処理するため、キューに含まれる最近受信したコマンドを (ある範囲内で) すべて処理し、その結果の合計で QoH を更新します。同時に表示されるコマンドは、1 つの更新にまとめられます。また、場合によっては、ユーザーが注文をキャンセルできるように、企業も注文をキャンセルできることを認識する必要もあります。

CQRS は強力なツールです。ただし、共有データ ストア、コラボレーション プロセス、DDD によって提供される戦略に適用すれば、大きな見返りがあります。


Peter Vogel は PH&V Information Services のシステム設計者兼社長です。PH&V は、UX 設計からオブジェクト モデリング、データベース設計まで、包括的なコンサルティングを提供しています。連絡先は peter.vogel@phvis.com.(英語のみ) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Dino Esposito および Julie Lerman に心より感謝いたします。
Dino Esposito は、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2014年) および『Modern Web Applications』(Microsoft Press、2016年) の著者です。JetBrains の .NET および Android プラットフォームのテクニカル エバンジェリストでもあります。世界各国で開催される業界のイベントで頻繁に講演しており、software2cents@wordpress.com (英語) や Twitter (@despos、英語) でソフトウェアに関するビジョンを紹介しています。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女は O'Reilly Media から出版されている『Programming Entity Framework』(2010 年) および『Code First』版 (2011 年)、『DbContext』版 (2012 年) を執筆しています。彼女の Twitter (@julielerman、英語) をフォローして、juliel.me/PS-Videos (英語) で彼女の Pluralsight コースをご覧ください。