web-dev-qa-db-ja.com

CQRSコマンドはクエリを実行する必要がありますか?

CustomerOrder、およびProductの集合ルートがあります...注文が作成されると、CustomerList<Sales.Items>。それは次のようになります:

public class Order
{
    public static Order Create(Customer customer, List<Sales.Item> items)
    {
        // Creates the order
        return newOrder;
    }
}

次に、CQRSを使用してOrderCreateHandlerを作成しました。

public class OrderCreateCommandHandler : IRequestHandler<OrderCreateCommand>
{

    public OrderCreateCommandHandler(ECommerceContext db)
    {
        _db = db;
    }

    public async Task<Unit> Handle(OrderCreateCommand request, CancellationToken cancellationToken)
    {
        var customerResult = // Q) Is it okay to execute a CustomerQuery here?

        var customer = new Customer(customerResult.Id, customerResult.FirstName, customerResult.MiddleName,
            customerResult.LastName, customerResult.StreetAddress, customerResult.City, customerResult.State, "United States",
            customerResult.ZipCode);

         //  blah....

         order = Order.Create(customer, products);
         _db.Orders.Add(order);
     }
}

私の質問はコマンドハンドラーにあります。クエリを実行してデータを取得し、渡す必要のある集計ルートを構築することはできますか?集約ルート自体を保存することはありませんが(必要な場合は参照のみ)、​​Idsとプリミティブをどこにも渡したくありませんSOLID OOと実際のオブジェクトを渡します。これはCQRSの違反ですか?

6
keelerjr12

ここで問題空間の簡単なレビューから始めましょう。 CQRSパターンを採用する基本的な利点は、書き込み側に同じモデルを使用しているときに発生し始めるインターリーブとリークを削減することにより、問題ドメインを解決/簡略化することです。読み取り側。多くの場合、発生する緊張は両方の不利益となります。 CQRSは、システムの書き込み側と読み取り側を分離(論理的および場合によっては物理的に分離)することで、この緊張を緩和しようとします。上記を念頭に置いて、コマンドもクエリも、反対側の「側面」からの論理エンティティに結合するべきではないことは明らかです。

上記のことから、質問に対する直接的な回答を作成することができます。はい、コマンドハンドラ内でデータストアをクエリできます提供コマンドモデルに対してクエリが発行されます。 OrderCreateCommandHandlerはアプリケーションのコマンドモデルの一部であるため、読み取りモデルのどの部分にも結合しないようにしたいと考えています。これがサンプルコードで大文字と小文字が区別されるかどうかは不明です(ただし、CustomerQueryという名前にはいくつかの疑いがあります)。

上記の答えよりも重要なのは、しかし、あなたが提供した例について他に感じていると感じることがあるということです。それも感じられますか?

ここで見るのは、かなりの結合です。ハンドラーがCustomerResult(VO?)を取得し、そのデータをすべて別のエンティティのコンストラクター(Customer)に分解してから、Customerをyetanotherエンティティのファクトリメソッドに渡します。ここではかなりの「質問」が行われています。つまり、結合を作成する方法で多くのデータを渡しています。

さらに、コマンドハンドラーは非常に宣言的な方法で "読み取り"ません(これは私たちが目指していることです)。つまり、多くの配管が邪魔をしているため、メソッドで何が行われているのかを「確認」するのが難しいということです。よりまとまりのある/宣言的な解決策を考え出すことができると思います。

コマンドハンドラの一般的な「フロー」は、3つの簡単なステップに分類できると仮定します。

  1. ユースケースを実行するために必要なすべてのデータ(ドメインモデル)を取得する
  2. ユースケースを満たすようにデータを調整する
  3. データを永続化する

より簡単な解決策を考え出せるかどうか見てみましょう。

_buyer = buyers.Find( cmd.CustomerId );

buyer.PlaceOrder( cmd.Products );

buyers.Save( buyer );
_

ああ!非常にきれい(3つの簡単なステップ)。さらに重要なのは、上記のコードが同じ目標を達成するだけでなく、異種オブジェクト間に多くの依存関係を作成することなく、より宣言的なおよびカプセル化された方法(何かを「更新」したり、ファクトリメソッドを呼び出したりすることはありません)。これを少しずつ分解して、上記がより良い解決策である「理由」を理解できるようにします。

buyer = buyers.Find( cmd.CustomerId );

私が最初に行ったのは、Buyerという新しい概念を導入することです。これを行うことで、動作に応じてデータをverticalに分割します。 CustomerエンティティにCustomer情報(FirstNameLastNameEmailなど)を保持させ、Buyerが購入を担当できるようにします。購入時に一部のCustomer情報を記録する必要があるため、Buyerをそのデータ(および場合によっては他のデータ)の「スナップショット」でハイドレートします。

buyer.PlaceOrder( cmd.Products );

次に、購入を調整します。上記のメソッドは、_new Order_が作成される場所です。 Orderは、どこからともなく現れるだけではありませんか? 何かはそれを配置する必要があるため、それに応じてモデル化します。これは何を達成しますか?まあ、_Buyer.PlaceOrder_メソッドは、BuyerNotInGoodStandingOrderExceedsBuyerSpendingLimit、またはRepeatOrderDetected例外をスローする場所をドメインに提供します。配置のコンテキストでOrderを作成するだけで、Orderの実現方法を強制できます。この例では、アプリケーションレイヤーのコマンドハンドラーまたはOrderファクトリーメソッドのいずれかで、各不変式を強制する必要があります。どちらもビジネスルールをチェックするのに適した場所ではありません。さらに、OrderPlacedイベントを発生させる場所があり(支払いコンテキストを分離するために必要です)、また、Orderエンティティを簡素化できます。これは、所有者への参照を維持するためにスカラーbuyerIdのみが必要になったためです。

buyers.Save( buyer );

かなり自明です。 Buyerには、OrderCustomerデータの「スナップショット」の両方を永続化するために必要なすべての情報が含まれるようになりました。データを内部で整理して永続化するためにどのように分解するかは、あなた次第です(ヒント:たとえば、Buyerを永続化する必要はありません。データに含まれるOrderだけです)。

[〜#〜]編集[〜#〜]

私が投稿したソリューションの例(それを呼び出すことができる場合)は、「ギアが回転する」ことを意図したものであり、必ずしも必ずしもが、手元の問題。つまり、your問題です。 Buyerをどのように配置できるかについての規則についての言及がなかった場合、Order集合体の概念を導入することは過度にエンジニアリングされている可能性があります。例えば:

_customer = customers.Find( cmd.CustomerId );

order = customer.PlaceOrder( cmd.Products ); // raise OrderPlaced

orders.Save( order );
_

完全に有効なアプローチかもしれません! CustomerInformationSlipに添付されているOrder(「スナップショット」)に必要な情報をすべて含めて、変更方法を制御する不変式を強制できるようにしてください。例えば:

_order.ChangeShippingAddress( cmd.Address ); // raise ShippingAddressChanged
_

上記のOrderExceedsCustomerShippingDistanceは、各Customerに、アカウント階層が指定された状態で出荷する距離に関する独自のルールがある場合にスローされる可能性があります。

ルールでデザインを決定させましょう!

17
king-side-slide

クエリを実行して、データを取得して、渡す必要のある集計ルートを構築することはできますか?

データの競合が心配ですか?

データの競合について心配していなければ、実際のデータの古いコピーを安全に使用できます。CQRSは、古いデータがどこから来たのかをそれほど気にしません。

一方、データの競合が問題になる場合は、停止して再考する必要があります。

この特定のの場合、クエリを使用しても問題ないでしょう。あなたが制御していない顧客情報を調べているように見えるので、クエリしているのは、現実の世界で制御されている情報だと思います。そのため、データがすでに間違っている可能性があります(顧客は名前を変更しましたが、まだ通知を受け取っていません)。

1
VoiceOfUnreason