web-dev-qa-db-ja.com

CQRSイベントソーシング:ユーザー名の一意性の検証

簡単な「アカウント登録」の例を見てみましょう。フローは次のとおりです。

  • ユーザーがウェブサイトにアクセス
  • 「登録」ボタンをクリックしてフォームに記入し、「保存」ボタンをクリックします
  • MVCコントローラー:ReadModelから読み取ってユーザー名の一意性を検証する
  • RegisterCommand:ユーザー名の一意性を再度検証します(ここに質問があります)

もちろん、MVCコントローラーのReadModelから読み取ることでUserNameの一意性を検証し、パフォーマンスとユーザーエクスペリエンスを向上させることができます。ただし、RegisterCommandで一意性を再度検証する必要があり、明らかに、コマンドでReadModelにアクセスしないでください。

イベントソーシングを使用しない場合は、ドメインモデルを照会できるため、問題ありません。ただし、イベントソーシングを使用している場合、ドメインモデルをクエリすることはできないため、RegisterCommandでUserNameの一意性を検証するにはどうすればよいですか

通知:ユーザークラスにはIdプロパティがあり、UserNameはユーザークラスのキープロパティではありません。イベントソースを使用している場合は、IDでのみドメインオブジェクトを取得できます。

BTW:要件で、入力されたUserNameがすでに使用されている場合、Webサイトには「申し訳ありませんが、ユーザー名XXXは使用できません」というエラーメッセージが表示されます。訪問者。 「お客様のアカウントを作成しています。しばらくお待ちください。後で登録結果をメールでお送りします」というメッセージを訪問者に表示することは許可されません。

何か案は?どうもありがとう!

[UPDATE]

より複雑な例:

要件:

注文時に、システムはクライアントの注文履歴を確認する必要があります。彼が貴重なクライアントである場合(クライアントが昨年1か月に少なくとも10件の注文をした場合、彼は貴重です)、注文に対して10%オフにします。

実装:

PlaceOrderCommandを作成し、コマンドで、注文履歴を照会して、クライアントが貴重かどうかを確認する必要があります。しかし、どうすればよいでしょうか。コマンドでReadModelにアクセスしないでください。 Mikael said の場合、アカウント登録の例では補正コマンドを使用できますが、この順序付けの例でもそれを使用すると、コードが複雑になりすぎて、コードの保守が難しくなる可能性があります。

71
Mouhong Lin

コマンドを送信する前に、読み取りモデルを使用してユーザー名を検証する場合、実際の競合状態が発生する可能性がある数百ミリ秒の競合状態ウィンドウについて話しています。これは私のシステムでは処理されません。それを処理するコストと比較すると、それは起こりそうにありません。

ただし、何らかの理由で対処する必要があると感じた場合、またはそのようなケースをマスターする方法を知りたいだけの場合は、次の1つの方法があります。

イベントソースを使用する場合は、コマンドハンドラーやドメインから読み取りモデルにアクセスしないでください。ただし、できることは、読み取りモデルに再度アクセスするUserRegisteredイベントをリッスンするドメインサービスを使用して、ユーザー名がまだ重複していないかどうかを確認することです。もちろん、ここでUserGuidを使用する必要があります。また、作成したユーザーで読み取りモデルが更新されている可能性があります。重複が見つかった場合は、ユーザー名の変更やユーザー名の取得をユーザーに通知するなど、補正コマンドを送信することができます。

それが問題への1つのアプローチです。

おそらくおわかりのように、これを同期の要求と応答の方法で行うことはできません。それを解決するために、SignalRを使用して、クライアントにプッシュしたいものがあるときはいつでもUIを更新しています(まだ接続されている場合)。私たちが行うことは、クライアントがすぐに見るのに役立つ情報を含むイベントをWebクライアントにサブスクライブさせることです。

更新

より複雑な場合:

コマンドを送信する前に、読み取りモデルを使用してクライアントが重要かどうかを確認できるため、注文の配置はそれほど複雑ではないと言えます。実際には、注文フォームをロードするときに、クライアントが注文する前に10%オフになることをクライアントに示す必要があるため、クエリを実行できます。 PlaceOrderCommandに割引を追加し、割引の理由を追加するだけで、利益を削減している理由を追跡できます。

ただし、繰り返しになりますが、注文が何らかの理由で行われた後で実際に割引を計算する必要がある場合は、OrderPlacedEventをリッスンするドメインサービスを再度使用します。この場合の「補正」コマンドはおそらくDiscountOrderCommandまたは何か。このコマンドはOrder Aggregateルートに影響を与え、情報が読み取りモデルに伝播される可能性があります。

ユーザー名が重複する場合:

ドメインサービスから補正コマンドとしてChangeUsernameCommandを送信できます。または、さらに具体的には、ユーザー名が変更された理由も説明します。これにより、Webクライアントがサブスクライブできるイベントが作成され、ユーザー名が重複していることがわかります。

ドメインサービスのコンテキストでは、他の手段を使用してユーザーに通知することもできます。たとえば、ユーザーがまだ接続しているかどうかわからないので便利なメールを送信するなどです。その通知機能は、Webクライアントがサブスクライブしているイベントとまったく同じイベントによって開始される可能性があります。

SignalRに関しては、ユーザーが特定のフォームを読み込むときに接続するSignalRハブを使用します。 SignalRグループ機能を使用して、コマンドで送信するGuidの値に名前を付けるグループを作成できます。これはあなたの場合userGuidかもしれません。次に、クライアントに役立つ可能性のあるイベントをサブスクライブするEventhandlerを用意します。イベントが到着すると、SignalRグループ内のすべてのクライアントでJavaScript関数を呼び出すことができます(この場合、1つのクライアントだけが重複したユーザー名を作成します)場合)。複雑に聞こえるかもしれませんが、実際はそうではありません。午後にすべてセットアップしました。 SignalR Githubページに優れたドキュメントと例があります。

36
Mikael Östberg

最終的な一貫性 への考え方や、イベントソーシングの性質についてはまだ理解していないと思います。私も同じ問題を抱えていました。具体的には、ドメインから割引が適用されることを確認せずに、例を使用して「この注文を10%割引で発注する」とクライアントからのコマンドを信頼する必要があることを受け入れませんでした。私にとって本当に気がかりなことの1つは ウディ自身が私に言ったこと (受け入れられた回答のコメントを確認する)でした。

基本的に、クライアントを信頼しない理由はないことに気づきました。読み取り側のすべてがドメインモデルから生成されているため、コマンドを受け入れない理由はありません。読み取り側で、顧客が割引の資格があると述べているものは、ドメインによってそこに置かれています。

ところで:要件では、入力したUserNameが既に使用されている場合、Webサイトは訪問者に「申し訳ありませんが、ユーザー名XXXは使用できません」というエラーメッセージを表示する必要があります。 「お客様のアカウントを作成しています。しばらくお待ちください。後で登録結果をメールでお送りします」というメッセージを訪問者に表示することは許可されません。

イベントソースと結果整合性を採用する場合は、コマンドを送信した直後にエラーメッセージを表示できない場合があることを受け入れる必要があります。固有のユーザー名の例を使用すると、これが発生する可能性は非常に低い(コマンドを送信する前に読み取り側を確認することを前提とする)ので、あまり心配する価値はありませんが、このシナリオでは後続の通知を送信する必要があります。次回のログオン時に別のユーザー名を使用します。これらのシナリオの優れている点は、ビジネスの価値と何が本当に重要かについて考えさせられることです。

UPDATE:2015年10月

ただ追加したかったのは、実際に公開されているWebサイトが関係しているということです。電子メールが既に取得されていることを示すことは、実際にはセキュリティのベストプラクティスに反することです。代わりに、登録は確認メールが送信されたことをユーザーに正常に通知したように見えますが、ユーザー名が存在する場合、メールはそのことを通知し、ログインまたはパスワードのリセットを促す必要があります。これが機能するのは、ユーザー名としてメールアドレスを使用している場合のみですが、このため、この方法をお勧めします。

23
David Masters

コマンドと同じトランザクションで更新される(たとえば、分散ネットワークを介さない)すぐに一貫性のあるいくつかの読み取りモデルを作成することに問題はありません。

分散ネットワーク上で読み取りモデルを最終的に整合させると、負荷の高い読み取りシステムの読み取りモデルのスケーリングをサポートするのに役立ちます。しかし、すぐに一貫性のあるドメイン固有の読み取りモデルを作成できないと言うことは何もありません。

即時整合性のある読み取りモデルは、コマンドを発行する前にデータをチェックして受信するためにのみ使用されます(実際にはコマンドに対するサービスです)。ユーザーに読み取りデータを直接表示する(つまり、GET Webリクエストなどから)ために使用することはできません。 )。そのためには、最終的には一貫性のあるスケーラブルな読み取りモデルを使用してください。

11
Gaz_Edge

他の多くのイベントのように、イベントソースベースのシステムを実装すると、一意性の問題が発生しました。

最初は、ユーザー名が一意であるかどうかを確認するために、コマンドを送信する前にクライアントがクエリ側にアクセスできるようにするサポーターでした。しかし、その後、一意性の検証がゼロのバックエンドを持つことは悪い考えであることがわかりました。システムを破壊するようなコマンドを投稿することが可能なのに、なぜ何かを強制するのですか?バックエンドはすべての入力を検証する必要があります。それ以外の場合は、一貫性のないデータに対して開いています。

私たちが行ったことは、コマンド側でindexを作成することでした。たとえば、一意である必要があるユーザー名の単純なケースでは、ユーザー名フィールドでUserIndexを作成するだけです。これで、コマンド側で、ユーザー名がすでにシステムに存在するかどうかを確認できます。コマンドが実行された後、新しいユーザー名をインデックスに保存しても安全です。

そのようなことが注文割引の問題でも機能する可能性があります。

利点は、コマンドバックエンドがすべての入力を適切に検証するため、一貫性のないデータを保存できないことです。

欠点は、一意性の制約ごとに追加のクエリが必要であり、複雑さが増すことです。

6
Jonas Geiregat

そのような場合、「期限付きのアドバイザリロック」のようなメカニズムを使用できると思います。

サンプル実行:

  • ユーザー名が存在するか、最終的に整合性のある読み取りモデルにないかを確認します
  • 存在しない場合;キーバリューストレージやキャッシュなどのredis-couchbaseを使用する。有効期限のあるキーフィールドとしてユーザー名をプッシュしてみてください。
  • 成功した場合;次にuserRegisteredEventを発生させます。
  • 読み取りモデルまたはキャッシュストレージにユーザー名が存在する場合は、ユーザー名を取得したことを訪問者に通知します。

SQLデータベースを使用することもできます。ユーザー名をロックテーブルの主キーとして挿入します。そして、スケジュールされたジョブが期限切れを処理できます。

5
Safak Ulusoy

一意性については、以下を実装しました。

  • 「StartUserRegistration」のような最初のコマンド。 UserAggregateは、ユーザーが一意であるかどうかに関係なく、ステータスがRegistrationRequestedで作成されます。

  • 「UserRegistrationStarted」では、非同期メッセージがステートレスサービス「UsernamesRegistry」に送信されます。 「RegisterName」のようなものになります。

  • サービスは、一意の制約を含むテーブルを更新しようとします(クエリなし、「教えないでください」)。

  • 成功した場合、サービスは、一種の承認「UsernameRegistration」を使用して別のメッセージで(非同期に)応答し、ユーザー名が正常に登録されたことを通知します。 (可能性は低いが)同時コンピテンスの場合に追跡するために、いくつかのrequestIdを含めることができます。

  • 上記のメッセージの発行者は、名前がそれ自体で登録されているという許可を持っているため、UserRegistration集約を安全に成功としてマークできるようになりました。それ以外の場合は、破棄済みとしてマークします。

まとめ:

  • このアプローチにはクエリは含まれません。

  • ユーザー登録は常に検証なしで作成されます。

  • 確認のプロセスには、2つの非同期メッセージと1つのdb挿入が含まれます。テーブルは読み取りモデルの一部ではなく、サービスの一部です。

  • 最後に、ユーザーが有効であることを確認する1つの非同期コマンド。

  • この時点で、非正規化子はUserRegistrationConfirmedイベントに反応して、ユーザーの読み取りモデルを作成できます。

3
Daniel Vasquez

「作業」キャッシュを一種のRSVPとして使用することを検討しましたか?少しサイクルで機能するので説明するのは難しいですが、基本的に、新しいユーザー名が「要求」されたとき(つまり、ユーザー名を作成するためにコマンドが発行されたとき)、ユーザー名を短い有効期限でキャッシュに配置します(キューを通過して読み取りモデルに非正規化された別のリクエストを説明するのに十分な長さ)。それが1つのサービスインスタンスである場合、メモリ内でおそらく機能します。それ以外の場合は、Redisなどで集中化します。

次に、次のユーザーがフォームに入力している間(フロントエンドがあると想定)、非同期で読み取りモデルでユーザー名が使用可能かどうかを確認し、ユーザー名がすでに使用されている場合はユーザーに警告します。コマンドが送信されると、コマンドを受け入れる前に(202を返す前に)要求を検証するために(読み取りモデルではなく)キャッシュをチェックします。名前がキャッシュにある場合は、コマンドを受け入れないでください。そうでない場合は、コマンドをキャッシュに追加します。追加に失敗した場合(他のプロセスがそれに打ち勝ったためにキーが重複する場合)、名前が使用されたと想定し、クライアントに適切に応答します。二つの間に、衝突の機会はあまりないと思います。

フロントエンドがない場合は、非同期ルックアップをスキップするか、少なくともAPIにルックアップするエンドポイントを提供させることができます。とにかく、クライアントがコマンドモデルと直接対話することを許可してはいけません。その前にAPIを配置すると、コマンドと読み取りホストの間のメディエーターとしてAPIを機能させることができます。

2
Sinaesthetic

おそらく、ここでは集計が間違っているようです。

一般的に、Yに属する値ZがセットX内で一意であることを保証する必要がある場合は、Xを集計として使用します。結局のところ、Xは不変量が実際に存在する場所です(XにはZが1つだけ存在できます)。

つまり、ユーザー名は、アプリケーションのすべてのユーザーのスコープ内で1回だけ表示される可能性があります(または、組織内などの別のスコープである可能性があります)。 「RegisterUser」コマンドを追加すると、「UserRegistered」イベントを保存する前に、コマンドが有効であることを確認するために必要なものが得られるはずです。 (そしてもちろん、そのイベントを使用して、「ApplicationUsers」集約全体をロードする必要なしにユーザーを認証するなどのことを行うために必要なプロジェクションを作成できます。

1
John Wilger