web-dev-qa-db-ja.com

CQRS-クエリが必要な場合、コマンドはどのように適切に検証できますか?

私はこの質問が何度も尋ねられたことを承知していますが、書き込み側からのクエリに関して、既存の質問では対処されていない、特にコマンドモデルの結果の一貫性に関する懸念がいくつかあります。

アプリケーション用のシンプルなCQRS + ESアーキテクチャがあります。顧客は私のサイトから商品を購入できますが、ハードコードされた要件があります。顧客は私たちのストアから500 $を超える製品を購入することはできません。彼らがしようとすると、購入は受け入れられないはずです。

だから、これは私のコマンドハンドラがどのように見えるかです(Pythonでは、単純化のために通貨、注入などの懸念から単純化されています):

class NewPurchaseCommand:
    customer_id: int
    product_ids: List[int]

class PurchasesCommandHandler:
    purchase_repository: PurchaseRepository
    product_repository: ProductRepository
    customer_query_service: CustomerQueryService

    def handle(self, cmd: NewPurchaseCommand):
        current_amount_purchased = self.customer_query_service.get_total(cmd.customer_id)

        purchase_amount = 0
        for product_id in cmd.product_ids:
            product = self.product_repository.get(product_id)
            purchase_amount += product.amount

        if current_amount_purchase + purchase_amount > 500:
             raise Exception('You cannot purchase over 500$')

        new_purchase = Purchase.create(cmd.customer_id, cmd.product_ids)
        self.purchase_repository.save(new_purchase)

        # Then, after the purchase is saved, a PurchaseCreated event is persisted, 
        # sent to a queue which will then update several read projections, which one 
        # of them is the underlying table that the customer_query_service uses.

CustomerQueryServiceは、基になるテーブルを使用して、現時点でユーザーが購入した金額をすばやく取得します。このテーブルは書き込み側でのみ使用され、最終的に更新されます。

CustomerPurchasedAmount table
CustomerId | Amount
10         | 480

コマンドハンドラーは単純なシナリオで動作しますが、起こりうるEdgeのケースを処理する方法を知りたいです。

  • このユーザー10は悪意のあるユーザーであり、20 $で同時に2つの購入を行います。しかし、CustomerPurchasedAmountテーブルが最終的に更新されるため、両方のリクエストが成功します(これが私が最も懸念しているケースです)。
  • リクエストの処理中に一部の製品の価格が変更される可能性があります(可能性は低いですが、その後も発生する可能性があります)。

私の質問は:

  • 以前に公開された同時実行のケースからコマンドをどのように保護できますか?
  • 書き込み側用に特別に調整されたモデルをどのように更新する必要がありますか?同期して?私が今やっているように非同期に?
  • そして一般的に、検証するためにクエリしている情報が古くなっている場合、コマンド検証はどのように行われるべきですか?

以前に公開された同時実行のケースからコマンドを回避および保護するにはどうすればよいですか?

同時変更から自分を「保護」する唯一の方法は、ロックを保持することです。これは、両方の変更を同じものの一部にすることを効果的に意味します。情報の配布を決定した後は、並行性は避けられません。

場合によっては、モデルを再考して不変値を操作することで軽減できます。たとえば、「今」価格を尋ねるのではなく、特定の時間の価格を尋ね、特定の時間に価格が1つだけであることを確認するための措置を講じます(見積もりまたは販売を考えてください。 2019-12-31)。

書き込み側用に特別に調整されたモデルをどのように更新する必要がありますか?同期して?私が今やっているように非同期に?

通常は非同期ですが、主に「依存します」。書き込み側で使用される「読み取りモデル」は、ローカルにキャッシュされたコピーに近い形式です。これにより、「キャッシュミスが発生した場合はどうなるのか」などの考え方が変わります。

正しい答えは失敗することもあれば、提案された変更を暫定的に受け入れて、情報が利用可能になるまで処理全体を延期することもある。

検証するために照会している情報が古くなっている場合、コマンド検証はどのように行われるべきか

私が見つけたのは、コマンド処理をハッピーパスに沿った遷移の線形シーケンスであると考えるのをやめ、代わりに処理を状態機械であると考え始める必要があるということです。

注文を受けたので、A、B、Cの価格を確認する必要があります。Aの価格が利用できるので、それを渡します。これで、BとCの価格が必要な状態になりました。他の確認はありません。制限時間内に完了するため、現在の作業を保存し、後で再開するようにスケジュールし、注文が処理中であることを示す応答を返します。

分散型自律サービスの利点が必要な場合は、集中制御の概念を手放す必要があります。

2
VoiceOfUnreason

VoiceOfUnreason の答えにはほぼ完全に同意します。コマンドハンドラーで検証する方法と、集計を設計する方法について少し追加します。

トランザクション境界としての集計:ドメイン主導の設計では、集計はトランザクション境界です。したがって、常に(最終的にではなく)一貫性が必要なすべての ビジネスの不変条件 は、集計レベルでテストする必要があります。

ユーザー集計のデザイン:これで、ユーザーが500ドルを超えて購入してはならないというルールができました。この場合、注文をユーザー集計の一部として追加します。新しい購入コマンドを取得すると、各アイテムのコストを計算し、ユーザー集計の注文エンティティを更新します。ユーザー集計を更新すると、Ordered domainイベントが発生します。システムの他の部分では、注文(現在のロジック)をさらに処理するためにドメインイベントを渡します。

大きな集約は集中化につながり、パフォーマンスを制限します:ドメイン主導の設計では、集約を小さく保つことに重点が置かれています。アグリゲートに対するすべての操作は、順番に行われます。したがって、集計が大きい場合、順次発生する多くの操作があります。上記の場合、次々と注文することができます。また、注文1の商品の価格の計算に時間がかかる場合、注文2の発行が遅くなります。

速度の代替設計:上記の設計は単純であり、パフォーマンスの制限で機能する場合は、それに固執する必要があると思います。提案された代替設計は、システムの複雑さを押し上げます。 Amazon Indiaと同様に、「注文の確認」などの注文後に別のステップを追加できます。したがって、ユーザーが注文すると、彼/彼女の注文を受け入れます。注文を受け入れた後、「UserOrders」などの新しい集計のビジネスルールを確認できます。これにより、500 $のビジネスルールを確保できます。これにより、システム内のアグリゲートの数(複雑さ)が増​​加します。また、注文確認メールと拒否メールが必要になる場合もあります(別の複雑さ)。この設計では、ユーザーは非常に速く注文できるようになります(メリット)。注文を確定するまで数分かかる場合があり、注文を確定/拒否するまで体はブロックされません。

読み取りモデルでビジネス不変条件を適用する際のリスク:読み取りモデルは最終的に更新されます。したがって、ビジネスの不変条件を適用するために読み取りモデルを使用している場合は、古い状態かどうかという質問に常に出くわします。そうは言っても、他の目的のためにコマンドハンドラーで読み取りモデルを使用できます。たとえば、注文の梱包が完了したらメールを送信します。電子メールアドレスを取得するために、ユーザーの読み取りモデルをクエリする場合があります。少し古いメールアドレスにメールを送信する場合は、ほとんどの場合問題ありません。