イベントソーシング-1つのアグリゲートでの変更に対して複数のイベントまたは単一のイベント?
CQRS / ES(イベントソーシング)を実装しているチェックリストシステムがあります。コマンドがあります
updateStatus(taskId: string, status: boolean)
タスクまたはサブタスクを完了としてマークします。サブタスクが完了したというコマンドを受け取り、すべての兄弟サブタスクも完了した場合、親タスクにも完了のマークを付ける必要があります。したがって、以下の例(タスクAのサブタスク1〜3)では:
- []タスクA-開く
- []タスク1-開く
- [*]タスク2-完了
- [*]タスク3-完了
タスクAと1は両方とも最初は開いていて、その後コマンドを受け取ります
updateStatus(task1, completed)
CommandHandlerは、イベントtaskCompleted(task1)を生成する必要があります。
私の質問は、正しいCQRS / ES要件は何ですか。
- 単一のイベントを生成します:taskCompleted(task1)
- 2つのイベントを生成します:taskCompleted(task1)、taskCompleted(taskA)
最初のオプションでは、イベントのコンシューマーは、アグリゲートも更新して完了する必要があることを確認する必要があります。第二に、コマンドハンドルがそれを処理します。
オプション1の主な欠点は、コマンドハンドラーの処理が増え、アグリゲートに関する知識が深まることです。もう1つの欠点は、イベントの再利用です(たとえば、完了時にタスク所有者に電子メールを送信するロジックがあるとします。オプション2では、イベントをリッスンし、知らないうちにイベントを処理する2番目のイベントハンドラーがあります。完全なロジック)。
オプション2の主な欠点は、イベントの数がはるかに多いことです。
CQRS / ESを使用するより正しいアプローチはどれですか?
回答
簡単な答え: 2つのイベントを生成する必要があります。
1回のコマンド呼び出しで複数のイベントが発生する可能性があるため、それらをさらに生成することは実際には問題ではありません。しかし、なぜあなたはあなたの場合にそれをしたいのですか?責任の分散を防ぐため。
非常に基本的なイベントソースのプロジェクトでは、アプリケーションに少なくとも2つの作業部分があると想像できます。
- イベントソースモデル、
- アプリケーションの読み取り側を更新して読み取り用のデータを生成するプロジェクター。
サブタスクが完了したというイベントを1つだけ生成した場合は、すべてのサブタスクの完了時に親タスクも完了するように、プロジェクターにロジックを導入する必要があります。ドメインロジックを複製しているのは、同じものが書き込み/ドメインレイヤーにも存在するため、すべてのサブタスクの完了時に親タスクの集計を完了するためです。その上、そのようなロジックは、ドメインとは完全に異なる言語で記述される可能性が非常に高くなります。たとえば、読み取りモデルがSQLデータベースにある場合はSQLで記述されます。
アプリケーションが私が説明した段階にある場合(つまり、読み取り側のプロジェクターで書き込み側)、ドメインロジックの複製は実際には問題ではないと言うかもしれません。結局のところ、多くのプロジェクトでは、SQL実装にドメインルールも含まれている場合があります。この問題は、アプリケーションが大きくなったり、マイクロサービス間で分割されたりすると、より明らかになります。
タスクが完了したときにすべてのウォッチャーにタスクを通知する通知マイクロサービスを追加すると、(サブタスク完了の)単一のイベントで、タスクの完了を判断する方法で、タスクのドメインロジックが再度コピーされます-すべてがローカルデータベースであるかどうかを確認しますサブタスクはすでに完了しています。これをさらに複雑にしているのは、プロジェクターとは異なり、このマイクロサービスは、タスク管理を含むマイクロサービスプロジェクトとは別に、まったく異なるプロジェクトに存在する可能性が非常に高いことです。これにより、インフラストラクチャ全体に散在していない壊れたドメインロジックを追跡することが非常に困難になります。
2つのイベントがあるため、プロジェクターで親タスクをマークするのは、次のようにするのと同じくらい簡単です。
fun changeTaskToCompleted(event: TaskCompletedEvent) {
database.executeUpdate('UPDATE task SET completed = true WHERE id = ?', event.taskId)
}
また、通知マイクロサービスでは、TaskCompletedEvent
:に反応するだけで実装も大幅に簡素化されます。
fun processEvent(event: Event) {
when(event) {
is TaskCompletedEvent -> sendTaskCompletedNotificationEmail(event)
}
}
@Andyによる回答で提起されたポイントに加えて、2つのイベントがある場合、すべての兄弟タスクが完了したかどうかのチェックがイベントハンドラーに移動するようにコードを配置できます。
これは行動の流れを作ります
- コマンドハンドラは受信します
updateStatus(task1, completed)
- コマンドハンドラーがイベントを発行します
taskCompleted(task1)
- TaskCompletedイベントハンドラーがTask1のイベントを受信します
- イベントハンドラーは、すべての兄弟タスクが完了したことを確認します
- イベントハンドラーがコマンドハンドラーにコマンド
updateStatus(taskA, completed)
を発行する、または - イベントハンドラーがイベントを発行します
taskCompleted(taskA)
- イベントハンドラーがコマンドハンドラーにコマンド
このように、コマンドハンドラーは、すべてのサブタスクが完了したときに親タスクの完了について知る必要さえありません。それはすべて専用のイベントハンドラーで処理されます。
オプション2の主な欠点は、イベントの数がはるかに多いことです。
CQRS / ESを使用するより正しいアプローチはどれですか?
持つ起こったさまざまなもののために複数のイベントはあり欠点ではないが、あなたのデザインを改善します。これにより、データの変更を解釈してビジネスの観点から何が起こったかを表現するロジックがサービスにカプセル化され、複数のプロジェクターに外部に漏れることはありません。アンディの答えはすでにこれを非常によく説明しています。
そしてもちろん、単一のコマンドが実行された後に複数のイベントを生成することはまったく問題ありません。これは、後続のイベントがどのようにトリガーされるかを実装の詳細です。
SubTaskCompletedイベントは、チェックは、タスクのすべてのサブタスクが完了しましたし、その後トリガされている場合は、そのいくつかの他のコードを引き起こす可能性TaskCompletedイベントを。ただし、サブタスクの完了のために発行される必要がある両方のイベントを決定するコマンドを実行する同じメソッド内にある可能性もあります。
注:メインタスク全体が完了したとチェックされた場合、そのようなサブタスクの進行状況はもはや面白くないため、メインタスク全体が個別のユーザー操作で完了した場合、後続のSubTaskCompletedイベントをトリガーしません。メインタスクをシングルクリックで完了したとマークした場合、イベントはシステムで実際に起こったことを反映するはずなので、私の観点からは、対応するすべてのサブタスクに対してサブタスク完了イベントを生成することは意味がありません。
あなたの質問と回答はイベントに強く焦点を当てていますが(もちろんそれは良いことです)、あなたの命令に関していくつかの潜在的な匂いが見られることを指摘したいと思います:
コマンドがあります
updateStatus(taskId: string, status: boolean)
タスクまたはサブタスクを完了としてマークします。
updateStatusはビジネス言語を反映していないため、ドメイン内で強い意味を持たないと確信しています。
コマンドを次のように変更することをお勧めします
completeSubTask(taskId: string)
これにより、コマンドに強い意味が与えられ、ビジネスロジックがより適切に表現されるだけでなく、イベントにも適合します。さらに、ブールフラグで始まり、その後、対応するビジネスロジックを理解するのがますます難しくなる、より多くのパラメーターに変更されるコマンド/メソッドをよく見ました。