CQRS。 事実と誤fall







CQRSは、読み取り操作を書き込み操作から分離するアーキテクチャスタイルです。 このアプローチは、Bertrand Meyerによって提案されたCQS原則に基づいてGreg Youngによって策定されました。 ほとんどの場合(常にではありませんが)CQRSは、DDDに基づいて設計されたアプリケーションの境界付きコンテキストで実装されます。 CQRSの開発の自然な理由の1つは、読み取りサブシステムと書き込みサブシステムでのビジネスロジックの負荷と複雑さの非対称的な分散であり、ほとんどのビジネスルールと複雑なチェックは書き込みサブシステムにあります。 同時に、彼らは変更するよりも頻繁に頻繁にデータを読み取ります。



概念は単純ですが、CQRSの実装の詳細は大きく異なる場合があります。 そして、これはまさに悪魔が詳細にある場合です。



ICommand



からICommand





多くの人は、「 チーム 」パターンを使用してCQRSの実装を開始し、1つのクラスでデータと動作を組み合わせます。



 public class PayOrderCommand { public int OrderId { get; set; } public void Execute() { //... } }
      
      





これにより、コマンドのシリアル化/逆シリアル化と依存性注入が複雑になります。



 public class PayOrderCommand { public int OrderId { get; set; } public PayOrderCommand(IUnitOfWork unitOfWork) { // WAT? } public void Execute() { //... } }
      
      





したがって、元のコマンドは「データ」-DTOと「コマンドハンドラ」の動作に分割されます。 したがって、「コマンド」自体には依存関係が含まれなくなり、以下を含むParameter Objectとして使用できます。 コントローラへの引数として。



 public interface ICommandHandler<T> { public void Handle(T command) { //... } } public class PayOrderCommand { public int OrderId { get; set; } } public class PayOrderCommandHandler: ICommandHandler<PayOrderCommand> { public void Handle(PayOrderCommand command) { //... } }
      
      





ハンドラー内で検証を行わないように、コマンド内のIdではなくエンティティーを使用する場合、この方法には欠点がありますが、 Model Bindingをオーバーライドできます。 少し後、標準のモデルBinidngを変更せずに検証をレンダリングする方法を見ていきます。

ICommandHandlerは常にvoidを返す必要がありますか?



ハンドラーは読み取りません。これは読み取りのためです。サブシステムとQueryの一部であるため、常にvoid



返す必要がありvoid



。 しかし、データベースによって生成されたIDはどうでしょうか? たとえば、「注文する」というコマンドを送信しました。 注文番号は、データベースのIDに対応しています。 IDは、 INSERT



要求が完了するまで取得できません。 人々が思い付かないのは、この架空の制限を回避することです。



  1. CreateOrderCommandHandler



    を連続して呼び出してから、 IdentityQueryHandler<Order&gt



  2. 出力-パラメーター
  3. コマンドへの戻り値に特別なプロパティを追加する
  4. イベント
  5. GUIDを優先して自動インクリメントIDを回避します。 ガイドはチームのボディに入ってデータベースに書き込まれます


さて、データベースへのクエリなしでは実行できない検証についてはどうでしょうか。たとえば、特定のIDまたはクライアントのアカウントの状態を持つデータベース内のエンティティの存在などです。 ここではすべてが簡単です。 検証に「例外」はないという事実にもかかわらず、ほとんどの場合、それらは単に例外をスローします。



グレッグヤングは、 この問題に関する自分の立場明確に述べています(25分):「 コマンドハンドラーは常に void



返すべきですか? いいえ、エラーまたは例外のリストは実行の結果である可能性があります。 ハンドラー 、操作の結果を返す場合があります。 Query



の作業に関与すべきではありません-データマイニングは、値を返すことができないことを意味しません。 これに関する主な制限は、システム要件と非同期相互作用モデルを使用する必要性です。 コマンドが同期的に実行されず、代わりにキューに入れられて後で処理されることが確実にわかっている場合は、HTTPリクエストのコンテキストでIdを取得することを期待しないでください。 Guid



操作とクエリステータスの取得、コールバックの提供、またはWebソケットを介した応答の取得を行うことができます。 どちらの場合でも、ハンドラー内のvoid



またはnon void



は、問題の少ない方です。 非同期モデルでは、インターフェイスを含むユーザーエクスペリエンス全体を変更する必要があります(OzonまたはAviasalesでの航空券の検索の様子をご覧ください)。



戻り値としてvoid



を使用すると、同期モデルと非同期モデルに1つのコードベースを使用できると期待しないでください。 意味のある戻り結果がないことは、APIの消費者を誤解させる可能性があります。 ところで、制御フローの例外を使用しても、ハンドラーから値を返しますが、暗黙的にそれを行うだけで、 構造プログラミングの原則に違反します



念のため、 DotNextの 1 つで、 Dino Espositoに意見を求めました。 彼はヤングに同意します。ハンドラーは応答を返すことができます。 void



ではないかもしれませんが、データベースのデータではなく、操作の結果である必要があります。 CQRSは、ドグマではなく、特定の状況(読み取りサブシステムと書き込みサブシステムの異なる要件)で利益をもたらす高レベルの概念です。

F#では、 void



void



境界線 void



さらに目立ちません。 F#のvoid



値はUnit



型に対応します。 関数型プログラミング言語のUnit



は、値のない一種のシングルトンです。 したがって、 void



void



違いは、抽象化ではなく技術的な実装によるものです。 Mark Simanのブログで void



unit



について詳しく読むことができます

クエリはどうですか?



CQRSのクエリは、 クエリオブジェクトに似ている場合があります。 ただし、実際にはこれらは異なる抽象化です。 クエリオブジェクトは、オブジェクトモデルを使用してSQLを生成するための特殊なパターンです。 .NETでは、 LINQ



Expression Trees



出現によりExpression Trees



パターンの関連性が失われました。 CQRSのQuery



は、クライアントにとって便利な形式でデータを受信するための要求です。



Command



CommandHandler



類推によりCommandHandler



Query



QueryHandler



を分離することは論理的QueryHandler



。 この場合、 QueryHandler



実際にvoid



返すことはできません。 要求時に何も見つからない場合は、 null



を返すか、 特別なケースを使用できnull







しかし、 CommandHandler<TIn, TOut>



QueryHandler<TIn, TOut>



の根本的な違いは何ですか? それらの署名は同じです。 答えは同じです。 セマンティクスの違い。 QueryHandler



はデータを返し、システムの状態を変更しません。 CommandHandler



CommandHandler



は状態を変更し、 場合によっては操作のステータスを返します。



セマンティクスが十分でない場合は、インターフェイスにこのような変更を加えることができます。



 public interface IQuery<TResult> { } public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { TResult Handle(TQuery query); }
      
      





TResult



型はさらに、クエリに戻り値があり、それをバインドすることも強調しています。 この実装は、Simple Injector開発者のブログと .NETのDependency Injectionの共著者であるStephen van Deyrsenのブログで見つけました。 実装では、オブジェクトのタイプを指定せずにリクエストが実行されていることをIDE画面ですぐに確認できるように、メソッド名をAsk



によるHandle



に置き換えることに限定しました。



 public interface IQueryHandler<TQuery, TResult> { TResult Ask(TQuery query); }
      
      





他のインターフェースが必要ですか?



ある時点で、他のすべてのデータアクセスインターフェイスが廃棄されるように見えるかもしれません。 いくつかのQueryHandler'



、それらからさらに多くのハンドラーを収集します。 QueryHandler'



は、ユースケースAとBが別々にあり、追加の変換なしでA + Bデータを返す別のユースケースが必要な場合にのみ意味があります。 戻り値のタイプによって、 QueryHandler



が何を返すかは必ずしも明らかではありません。 そのため、さまざまな汎用パラメーターを持つインターフェイスで混乱するのは簡単です。 さらに、C#は冗長です。



 public class SomeComplexQueryHandler { IQueryHandler<FindUsersQuery, IQueryable<UserInfo>> findUsers; IQueryHandler<GetUsersByRolesQuery, IEnumerable<User>> getUsers; IQueryHandler<GetHighUsageUsersQuery, IEnumerable<UserInfo>> getHighUsage; public SomeComplexQueryHandler( IQueryHandler<FindUsersQuery, IQueryable<UserInfo>> findUsers, IQueryHandler<GetUsersByRolesQuery, IEnumerable<User>> getUsers, IQueryHandler<GetHighUsageUsersQuery, IEnumerable<UserInfo>> getHighUsage) { this.findUsers = findUsers; this.getUsers = getUsers; this.getHighUsage = getHighUsage; } }
      
      





QueryHandler



を特定のユースケースのエントリポイントとして使用する方が便利です。 内部でデータを受信するには、専用のインターフェイスを作成します。 したがって、コードはより読みやすくなります。

小さな関数を大きな関数にコンパイルするという考えが気にならなければ、プログラミング言語の変更を検討してください。 F#では、このアイデアがはるかに具体化されています。

書き込みサブシステムは読み取りサブシステムを使用できますか?



別の教義は、書き込みサブシステムと読み取りサブシステムを混在させないことです。 厳密に言えば、ここではすべてが真実です。 コマンドハンドラー内でQueryHandler



からデータを取得する場合は、このサブシステムではCQRSが不要であることを意味します。 CQRSは特定の問題を解決します。読み取り-サブシステムは負荷を処理できません。



最近までDDDグループで最も人気のあった質問の1つは、「DDDを使用しており、ここに年次報告書があります。 ビルドしようとすると、ビジネスロジックレイヤーがRAM内の集計を上げ、RAMが不足します。 どのようになりますか?」 最適化されたSQLクエリを手動で記述する方法は明らかです。 同じことが訪問済みWebリソースにも当てはまります。 データ、キャッシュ、および表示を受信するためにすべてのOOPの素晴らしさを上げる必要はありません。 CQRS-優れた分岐点を提供します。コマンドハンドラーではドメインロジックを使用します。チームがあまり多くないため、またすべてのビジネスルールチェックを実行するためです。 逆に、読み取りサブシステムでは、ビジネスロジックの層をバイパスすることが望ましいのです。



読み取りサブシステムと書き込みサブシステムを組み合わせることにより、分岐点を失います。 セマンティック抽象化の意味は、1つのリポジトリのレベルでも失われます。 read-サブシステムが異なるデータウェアハウスを使用する場合、システムが一貫した状態にあるという保証はまったくありません。 データの関連性は保証されないため、ビジネス層のチェックの意味は失われます。 読み取りサブシステムで書き込みサブシステムを使用すると、通常、操作の意味と矛盾します。定義によりコマンドはシステムの状態を変更し、クエリは変更しません。



ただし、各ルールには例外があります。 同じビデオの1分前に、グレッグは例を示します。「計算を行うには、数百万のエンティティを読み込む必要があります。 このすべてのデータをRAMに読み込むか、最適なクエリを実行しますか?」 読み取りサブシステムにすでに適切なクエリハンドラがあり、1つのデータソースを使用している場合、コマンドハンドラからクエリを呼び出すために誰も刑務所に入れられません。 これに対する議論に留意してください。



QueryHandler



からエンティティまたはDTOをQueryHandler



ますか?



DTO。 クライアントがデータベースのユニット全体を必要とする場合、クライアントに何か問題があります。 さらに、可能な限りフラットなデータが通常必要です。 プロトタイピング段階でLINQとQueryable ExtensionsまたはMapsterの使用を開始できます 。 また、必要に応じて、 QueryHandler



実装をDapperおよび/または別のデータストアに置き換えます。 Simple Injectorには便利なメカニズムがあります。アセンブリからオープンジェネリックインターフェイスを実装するすべてのオブジェクトを登録し、残りはLINQでフォールバックのままにしておくことができます。 この構成を作成したら、編集する必要はありません。 アセンブリに新しい実装を追加するだけで十分で、コンテナが自動的に取得します。 他のジェネリックについては、LINQ実装のフォールバックが引き続き機能します。 Mapster



Mapster



では、マッピング用のプロファイルを作成する必要はありません。 Entity



Dto



間のプロパティ名の規則に従うとDto



プロジェクションは自動的に構築されます。

「自動マッパー」では、次のルールがあります。手動マッピングを作成する必要があり、組み込みの合意が十分でない場合は、自動マッパーなしで行うことをお勧めします。 したがって、「マップスター」への移行は非常に簡単であることがわかりました。


CommandHandlerとQueryHandler- 全体的な抽象化



すなわち トランザクションの開始から終了まで有効です。 すなわち 通常の使用-要求ごとに1つのハンドラー。 データにアクセスするには、すでに言及したQueryObject



UnitOfWork



など、他のメカニズムを使用することをおUnitOfWork



ます。 ちなみに、これはCommand



からQuery



を使用する問題と、その逆の問題を解決します。 QueryObject



使用するだけです。 この規則に違反すると、トランザクションの管理とデータベースへの接続が複雑になります。



横断的関心事とデコレータ



CQRSには、標準のサービスアーキテクチャよりも大きな利点が1つあります。汎用インターフェイスは2つしかありません。 これにより、「 decorator 」テンプレートの有用性を大幅に高めることができます。 アプリケーションには必要ですが、文字通りの意味ではビジネスロジックではない多くの関数があります。ロギング、エラー処理、トランザクション性などです。 従来、2つのオプションがあります。



  1. このような依存関係と関連コードを使用してビジネスロジックを配置し、整理します。
  2. AOPに目を向ける: Castle.Dynamic Proxy



    などのランタイムスポイラーを使用するか、 Castle.Dynamic Proxy



    などのコンパイル時にILを書き換える


最初のオプションは、冗長性とコピーアンドペーストが悪いです。 2番目-パフォーマンスとデバッグの問題、外部ツールへの依存、および「魔法」。 デコレータ付きオプション-中央のどこかにあります。 一方では、デコレータの付随するロジックを取り出すことができます。 一方、魔法はありません。 すべてのコードは人によって作成され、デバッグできます。



ModelBinderを変更せずに入力パラメーターを検証することで問題を解決することを約束しましたか? 答えは、検証用のデコレータを実装することです。 例外の使用に慣れている場合は、 ValidationExcepton



スローします。



 public class ValidationQueryHandlerDecorator<TQuery, TResult> : IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { private readonly IQueryHandler<TQuery, TResult> decorated; public ValidationQueryHandlerDecorator(IQueryHandler<TQuery, TResult> decorated) { this.decorated = decorated; } public TResult Handle(TQuery query) { var validationContext = new ValidationContext(query, null, null); Validator.ValidateObject(query, validationContext, validateAllProperties: true); return this.decorated.Handle(query); } }
      
      





そうでない場合は、小さなラッパーを作成し、戻り値としてResultを使用できます。



  public class ResultQueryHandler<TSource, TDestination> : IQueryHandler<TSource, Result<TDestination>> { private readonly IQueryHandler<TSource, TDestination> _queryHandler; public ResultQueryHandler(IQueryHandler<TSource, TDestination> queryHandler) { _queryHandler = queryHandler; } public Result<TDestination> Ask(TSource param) => Result.Succeed(_queryHandler.Ask(param)); }
      
      





SimpleInjectorは、 オープンジェネリックとデコレータ登録する便利な方法を提供します。 1行のコードで、実行前、実行後、グローバルトランザクションのハング、エラー処理、ドメインイベントへの自動サブスクリプションを記録できます。 主なことは無理をしないことです。



IQueryHandler



ICommandHandler



2つのインターフェイスには不便なIQueryHandler



ありICommandHandler



。 両方のサブシステムでロギングまたは検証を有効にするには、同じコードで2つのデコレーターを記述する必要があります。 まあ、これは典型的な状況ではありません。 読み取りサブシステムでは、トランザクション性が必要になることはほとんどありません。 それでも、検証とロギングを使用した例は非常に重要です。 この問題は、インターフェイスからデリゲートに移行することで解決できます。



 public abstract class ResultCommandQueryHandlerDecorator<TSource, TDestination> : IQueryHandler<TSource, Result<TDestination>> , ICommandHandler<TSource, Result<TDestination>> { private readonly Func<TSource, Result<TDestination>> _func; //      protected ResultCommandQueryCommandHandlerDecorator( Func<TSource, Result<TDestination>> func) { _func = func; } //  Query protected ResultCommandQueryCommandHandlerDecorator( IQueryHandler<TSource, Result<TDestination>> query) : this(query.Ask) { } //  Command protected ResultCommandQueryCommandHandlerDecorator( ICommandHandler<TSource, Result<TDestination>> query) : this(query.Handle) { } protected abstract Result<TDestination> Decorate( Func<TSource, Result<TDestination>> func, TSource value); public Result<TDestination> Ask(TSource param) => Decorate(_func, param); public Result<TDestination> Handle(TSource command) => Decorate(_func, command); }
      
      





はい、この場合、小さなオーバーヘッドもあります。コンストラクターに渡されたパラメーターをキャストするためにのみ、2つのクラスを宣言する必要があります。 これはIOCコンテナの構成を複雑にすることでも解決できますが、2つのクラスを宣言する方が簡単です。



別の方法は、 Command



およびQuery



IRequestHandler



インターフェイスを使用し、混乱を避けるために命名規則を使用することです。 このアプローチはMediatRライブラリに実装されてます。



All Articles