web-dev-qa-db-ja.com

CQRSによるコマンド処理の失敗のフィードバック

CQRSアプローチを使用していくつかのコンテキストを開発しています。最終的に、コマンドハンドラーがイベントを発行するようになりました。それは私たちには良い考えではないようです。ただし、代替のアプローチを見つけることはできません。私たちは2つの特定のシナリオグループと格闘します。

  1. 新しい集計を作成しています。

アグリゲートを作成すると、失敗する場合があります。成功した場合、すべてが簡単です。aggregateはイベントAggregateCreatedを保持します(コンストラクター内でビルド)。ただし、失敗した場合、aggregateは存在しないため、何も出力/公開できません。このような場合、コマンドハンドラーは、ドメインリークと見なされるCreatingAggregateFailedを作成します。

  1. リソースが見つかりません

2番目のシナリオは、特定のリソースを設立しないことに関するものです。たとえば、存在しないリソースを削除します。私たちの実装では、Repository :: find()はNotFound例外を発生させます。 excetpinonは、AggregateNotFoundイベントを発行するハンドラーでキャッチされます。

これらのイベントに基づいて、プロセスマネージャーと応答を構築します。しかし、ドメイン(アプリケーション?)イベントがアグリゲートの外部に出力されるのは少し厄介に見えます。これらの厄介なイベントは、そのような集合体が存在しない場合にも当てはまります。

最も単純なシナリオを考えてみましょう。 AddTeamMember($ teamGuid、$ userGuid、$ role)コマンドをディスパッチします。チーム、ユーザー、ロールは存在するが、コマンドが集計不変のいずれかに違反している場合、集計はMemberRejectedイベントを登録する可能性があります-すべて良好です。しかし、ユーザー、チーム、または与えられた役割のどちらも存在してはいけません。したがって、イベントを登録できる集約はありません。適切なアクションを実行するには(プロセスマネージャーで、またはコマンド発行者に通知するため)、この失敗に関するフィードバックが必要です。 MembershipRequest集計をコマンドレシーバーと見なします。次に、イベントを公開するために常に有効な集計を使用しており、イベントは集計内で意味があります。しかし、これは追加の柔軟性をもたらします。可能なコマンドごとに「リソースが見つかりません」を処理するための中間集計が必要です。

新しいアイデアを思いついた。これをコードサンプルで説明します。

古いハンドラーバージョン

/**
 * @param CreateChannel $command
 */
protected function handleCreate(CreateChannel $command): void
{
    try {
        $channel = new Channel($command->getGuid(), $command->getSymbol(), $command->getLangCodes());
        $this->channelRepository->save($channel);
    } catch (InvalidData $e) {
        $this->eventBus->publish($channel, new ChannelCreationFailed($command->getGuid()));
    }
}

新しい考え

/**
 * @param CreateChannel $command
 */
protected function handleCreate(CreateChannel $command): void
{
    try {
        $channel = new Channel($command->getGuid());
        $channel->create($command->getSymbol(), $command->getLangCodes()))
        $this->channelRepository->save($channel);
    } catch (InvalidChannelData $e) {
        // ?
    } finally {
        $this->eventBus->publish($channel, new ChannelCreationFailed($command->getGuid()));
    }
}

しかし、これにはいくつかの明らかな欠点があります。まず第一に、サービス層は集約設計に影響を与えます。また、オブジェクト言語の概念の作成を再発明します。集約が「作成済み」状態であるかどうかを他のメソッド内でチェックすることを強制します。オブジェクトの作成と集約の作成を区別するのは良い点でしょうか?このアプローチでは、コンストラクターから発生する例外は想定されていません。コンストラクターは常に、有効な(コマンドによって保証される)GUIDのみを取得し、他には何も取得しません。

常にguidを使用して集計を行うと、存在しない参照されている集計も解決されます。二重ディスパッチを使用すると、ソース集約から「見つかりません」例外を発生させることが可能です

$team->addMember($userGuid, $usersRepository);

しかし、このmakse集約のapiは、

$team->addMember($user);
3
ayeo

ドメインの専門家が気にしないドメインイベントを公開しないでください。

特定の障害事例がビジネスプロセスの特定された部分である場合(通常、ビジネススタッフとのイベントストーミングセッション中に発生するもの)、ユビキタス言語でその用語を見つけ、そのためのイベントを確実に公開します。

しかし、ネットワークの中断、システムの停止、構成の誤りなどのような非通常的なケースでは、通常のpub-subサイクルを実行しません。 UIを介してユーザーが要求するコマンドの場合は、エラーが発生したことを通知します。コマンドがプロセスマネージャによって実行された場合、何かがうまくいかなかったことがわかります。

  • リトライ
  • 補正コマンドを実行する
  • または管理者に通知

状況に応じて。補正コマンドが送信され、結果として新しいイベントが発行された場合、相関IDは、補正をトリガーした元のイベントまたはコマンドを追跡し、ログを利用して問題を診断および修正できます。

2
guillaume31

イベントソースでは、通常、モデルの状態が変化した場合にのみイベントを記録します。したがって、「WeDidn'tChangeTheStateOfTheModel」イベントはあまり意味がありません。

ただし、処理される要求や失敗などに非常に注意を払うtelemetryと呼ばれる横断的関心事があります。しかし、それは異なるドメインであり、完全に異なるコストのトレードオフがあります

(例:顧客の注文を失うことは悪いため、domainイベントを失うことによるビジネスへのコストは高くなります。ただし、注文が正常に処理されたことを通知するテレメトリーイベントの数が対応するドメインイベントの数と正確に一致します。おそらく、ビジネスにそれほどコストはかかりません)。

ビジネスは通常、Webログよりも注文の永続的な履歴を重視します。

はい、コマンドハンドラーがドメインイベントではなくテレメトリイベントを発行することを期待しています。

3
VoiceOfUnreason

質問のために、ドメインと技術の2種類の障害を区別してみましょう。

学生がコースに登録したが、一部のバックエンドサービスが失敗したとします。この場合、StudentCreated()、StudentDebited()、およびClassRoomSeatReserved()イベントがイベントストアに永続化されている必要があるため、失敗したサービスがオンラインに戻ったときに、イベントを再実行できます。

技術例外はログに記録され、theDevOpsによって取得され、失敗したイベントバッチへの参照が含まれているはずです。これは、可能性のある佐賀との結果整合性です。

サービスがコマンドを受け取ったら、コマンドの検証を行い、ドメインエラーを呼び出し元に送り返す必要があります。また、ドメインに関心がある場合は、オプションでドメインイベントを作成する必要があります。

2つのプロセスは別個であり、目的が異なります。

これへのAggregateの関与(たとえば、StudentAggegate())に関しては、イベントが永続化されてソース化された後にのみ関与します。

Aggregateはデータベース内のレコードではなく、境界コンテキスト内の兄弟の動作をすべて制御するクラスです。

ファクトリパターンと流暢なインターフェイスを使って、次のように説明しましょう。

If(!StudentRegisterCommand.IsModelValid())

// optionally create domain events.
return ModelValidationErrors() 

eventlist = Factory.CreateStudent(StudentRegisterCommand).Debit(StudentRegisterCommand.Cource.Cost、StudentRegisterCommand.Card).ReserveSeat(StudentRegisterCommand.Cource).Events()

Repository.PersistEvents(イベントリスト)

Bus.AddEvents(イベントリスト)

これで初めて、RegistrarServiceがイベントを受信して​​、新しいStudentAggegateを作成します。デビットが失敗した場合、座席の予約が取り消され、FinanceServiceは次のことを行います。

Student = StudentService.GetStudent()

eventlist = Factory.NotifyStudent(Student.Name).Message(「カードが拒否されたため、ご連絡ください。」)。ByMail(Student.email).SMS(Student.Phone).UnReserveSeat(Student、Student.Cource)

Repository.PersistEvents(イベントリスト)

Bus.AddEvents(イベントリスト)

集合体はクラスであり、

お役に立てれば。

1
Marius

オブジェクト指向とサービスアプローチの混在によって引き起こされた問題のようです。

OOアプローチを取り、イベントがオブジェクトによって発行されるようにしたい場合、オブジェクトはCommandオブジェクトとQueryオブジェクトをカプセル化する必要があります。

  • Object.Save()/ ObjectCollection.New()はコマンドを作成し、コマンドが失敗した場合にイベントをスローする必要があります

  • Object.Get()/ ObjectCollection.Find()はクエリを作成し、失敗した場合はnot foundイベントをスローします。

サービスのアプローチを取る場合、イベントはサービスまたはリポジトリに属し、集約の一部ではありません。

とにかくドメインを参照する必要があるので、これらのオブジェクトが孤立したドメインイベントを作成することは問題ではないと思います。そうすることで、異なるドメイン/アグリゲート間のカップリングは発生しません

OOアプローチに固執する場合は、リポジトリをObjectCollectionに挿入します。リポジトリにCQRSスタッフをカプセル化させ、ObjectCollection内のイベントに変換される独自のエラーをスローさせます。

ObjectCollection自体は常に空のコレクションとして存在できます。

0
Ewan