プレイブック
CQRS (コマンドクエリ責任分離) パイプラインはどのように機能しますか?コマンドとクエリのフローの構造 (CQRS)
CQRS リクエスト パイプラインとは何ですか? HTTP リクエストは、コントローラー、MediatR、パイプライン動作、ハンドラー、送信ボックス、読み取りモデルをどのように通過するのでしょうか?
CQRS - 意思決定の構造
一部 2 の 4
CQRS はフレームワーク構文を使用しません。意思決定、規模、生産の緊張関係を考察する 4 部構成のエンジニアリング本。
HTTP リクエストがシステムに届くと何が起こるでしょうか?検証はどこで行われ、トランザクションはいつ開かれ、ドメイン ルールはどのレイヤーで適用されますか? CQRS を使用するシステムでは、このプロセスは従来の CRUD アーキテクチャとは異なります。この記事では、リクエストが API からデータベース、読み取りモデルに至るまでの手順を調べます。
CQRS に詳しくない読者にとって、最も短い辞書は次のとおりです。 コマンド はシステムを変更する意図です。 クエリ は、システムを変更せずに情報を要求します。 ハンドラー は、このリクエストのハンドラーです。 MediatR のようなメディエーターを使用すると、コントローラーは正しいハンドラーを直接認識するのではなく、リクエストを適切なストリームに転送できます。```text POST /orders ↓ Controller ↓ Mediator.Send(command) ↓ Pipeline behaviors ↓ Command handler ↓ Aggregate + Repository ↓ Database
パイプラインの動作は一般的な技術的な制御であり、すべてのコマンドで書き直す必要はありません。これらはいずれもビジネス ルールではありません。ビジネス ルールはハンドラーと集約内に存在します。```text
ValidationBehavior
↓
AuthorizationBehavior
↓
LoggingBehavior
↓
MetricsBehavior
↓
RetryBehavior (yalnızca güvenli işlemlerde)
↓
TransactionBehavior
↓
Handler
```トランザクションは最後に開始されます。データベース接続を不必要に移動したり、無効、未承認、または途中で拒否される可能性のあるリクエストをロックしたくないからです。 Handler に到達すると、ビジネス上の決定をアトミックに実装する準備が整います。
**Aggregate** は、決して破ってはいけないシステムのビジネス ルール、つまり不変条件を保持します。たとえば、完了した注文を再度キャンセルできない場合、このルールはコントローラーではなく注文集合体で維持される必要があります。
クエリ側は別のパスをたどります。同じ注文の場合、ユーザーが必要とするのはドメイン オブジェクト全体ではなく、画面に適した短いビューである可能性があります。```text
GET /orders
↓
Authorization
↓
Cache (uygunsa)
↓
Read database / Search index
↓
OrderSummary DTO
↓
Frontend
最初に説明した概念```text
📦 Handler Tek bir command veya query'nin uygulama iş akışını yürütür.
📦 Mediator Controller'ın handler'ı doğrudan bilmeden isteği doğru işleyiciye göndermesini sağlar.
📦 Pipeline Behavior Validation, authorization veya logging gibi her istekte çalışan ortak teknik katmandır.
📦 Repository Aggregate'i yükleyen ve kalıcı olarak saklayan uygulama sınırıdır.
📦 DTO Domain modelin tamamını değil, bir ekranın ihtiyacı olan veriyi taşır.
ハンドラーの例を読むときは、この区別に留意してください。ハンドラーは単にビジネス上の決定を実装するだけです。検証、認可、ロギングはハンドラーに含まれていません。これらはパイプラインの動作によって解決されます。このようにして、ハンドラーは小さいままであり、各リクエストは同じセキュリティ/トレーサビリティ ルールを通過します。
CQRS は、いくつかの MediatR ハンドラーとキャッシュという 2 つのフォルダーとして認識されることがよくあります。この画像は誤解を招きます。 CQRS の主な仕事はコードを分割することではありません。これは、**システムの状態を変更する決定**と**その状態に関する情報を要求する要求**を区別するためです。
これは、CQRS-Anatomy of Decisions コレクションの 2 番目の部分です。最初の部分では、なぜ分離が必要になったのかについて話します。ここで、メカニズムについて説明します。コマンドはどのゲートを通過するのか、クエリはなぜ同じパスをたどるべきではないのか、読み取りモデルはいつ別のシステムになるのかなどです。
## 開始点: 1 つのリクエスト、2 つの異なる意図
`CreateOrder` はコマンドです。彼はシステムに新しい現実を加えたいと考えています。ルールを実行し、承認を要求し、トランザクションをオープンし、監査可能である必要があります。
`GetOrderSummary` はクエリです。それは新しい現実を生み出すわけではありません。高速で狭く、画面に優しいビューを返したいと考えています。同じ集約をロードしたり、ドメイン ルールを実行したり、書き込み側のロックと競合したりする必要はありません。
この区別のアルゴリズム上の意味は明らかです。コマンド側では、検証と一貫性のためにコストが考慮されることがよくあります。クエリ側では、クエリされるデータの量に応じてコストが増加します。リスト画面の集計グラフを走査する代わりに、調整された読み取りモデルを使用すると、不必要な I/O と認知負荷の両方が軽減されます。
## コマンド パイプライン: 意思決定への安全なパス
コマンド側の目的は、`handler` を呼び出すことだけではありません。リクエストは、ビジネス上の決定に至る前に、システムの共通ルールを通過する必要があります。```text
API
→ Authentication / Authorization
→ Validation
→ Idempotency check
→ Transaction
→ Command Handler
→ Aggregate + Domain Rules
→ Persist + Outbox
→ Commit
```疑似コード:```text
handle(command):
authorize(command.actor)
validate(command)
return idempotency.execute(command.key):
begin transaction
aggregate = repository.load(command.aggregateId)
aggregate.apply(command)
repository.save(aggregate)
outbox.store(aggregate.domainEvents)
commit transaction
```各ステップには単一の責任があります。検証はビジネス ルールを置き換えるものではありません。無効なフォームを早期に拒否します。承認では、決定が所有者によって所有されているかどうかがチェックされます。集約は不変条件を保持します。一方、Outbox では、永続的な状態と発行されるイベントが同じトランザクション制限内で発生することが保証されます。
C# 側では、Mediator によってこのフローが実用的になります。```csharp
public sealed record PlaceOrder(Guid CustomerId, IReadOnlyList<OrderLine> Lines) : IRequest<OrderId>;
public sealed class PlaceOrderHandler : IRequestHandler<PlaceOrder, OrderId>
{
public async Task<OrderId> Handle(PlaceOrder command, CancellationToken ct)
{
var order = Order.Place(command.CustomerId, command.Lines);
await repository.AddAsync(order, ct);
await outbox.AddAsync(order.DomainEvents, ct);
return order.Id;
}
}
```監査、測定、検証、再試行などの横断的な責任がパイプライン動作に移されるため、ハンドラーは短いままです。この利点は、すべてのハンドラーが同じ動作を手作業で再現するわけではないことです。その代償として、コール チェーンの可視性が失われるリスクが伴います。したがって、一連の行動を明確に文書化し、観察する必要があります。
## クエリ パイプライン: 必要なだけ読み取ります
クエリ側の基本的な質問は、ユーザーがどのようなレイテンシ バジェット内でどのような情報を必要としているかということです。この質問に対する答えは、ドメイン モデルではなく、使用シナリオにあります。```text
API
→ Authorization for the view
→ Query Handler
→ Read model / cache / search index
→ DTO shaped for the screen
```多くの場合、注文リストの `Order` 集計、顧客関係、在庫ルールを再ロードする必要はありません。クエリ ハンドラーは、画面に必要なフィールドを直接選択します。したがって、コストはドメイン グラフ全体ではなく、返される行と列の数にほぼ制限されます。```csharp
public sealed record GetOrderSummary(Guid OrderId) : IRequest<OrderSummaryDto?>;
public sealed class GetOrderSummaryHandler : IRequestHandler<GetOrderSummary, OrderSummaryDto?>
{
public Task<OrderSummaryDto?> Handle(GetOrderSummary query, CancellationToken ct) =>
readDb.OrderSummaries
.Where(x => x.Id == query.OrderId)
.Select(x => new OrderSummaryDto(x.Id, x.Status, x.Total, x.UpdatedAt))
.SingleOrDefaultAsync(ct);
}
```ここで、`DTO` はドメインを隠すための装飾ではありません。読書同意書です。画面が切り替わると読み込むモデルが変わる場合があります。ビジネス ルールが変更されると、コマンド モデルも変更される可能性があります。 2 つの変化は同じペースで起こる必要はありません。
## 論理 CQRS と物理 CQRS
最初のステップは、ほとんどのチームにとって論理 CQRS です。コマンドとクエリはコード内で分離されていますが、同じリレーショナル データベースを使用します。 ACID トランザクションは保護され、運用コストは低く、デバッグは簡単です。
物理 CQRS では、読み取りモデルは別のデータ ストア、キャッシュ、または検索インデックスに移動されます。これにより、読み取り側を独立してスケーリングする機会が得られます。しかし、その代わりに、データの鮮度、投影エラー、再構成プロセスが発生します。したがって、物理的な分離はパフォーマンス上の空想ではなく、実証済みのボトルネックに対する解決策です。
|選挙 |収益 |承認された価格 |
| --- | --- | --- |
|論理 CQRS |低い運用コスト、強力な一貫性 |読み取りと書き込みは同じインフラストラクチャを共有します。
|物理的な CQRS |独立したスケーリング、画面にフィットするモデル |最終的な整合性、射影操作 |
## 書き込みモデルから読み取りモデルへ: CDC または送信ボックス?
物理的な分離における重要な問題は、データがどのように流れるかです。変更データ キャプチャは、ログからデータベースの変更をキャプチャできます。既存のシステムに手を加えずに再生するには強力です。ただし、これにより、メッセージ コントラクトがデータベース スキーマに近づきます。
送信ボックスのアプローチでは、同じトランザクションでドメイン イベントを送信ボックス テーブルに書き込みます。リレーはこれらの記録をブローカーに送信します。消費者はべき等に動作します。したがって、`database write başarılı, event publish başarısız` のジレンマは対処可能になります。しかし、リレー、再試行、デッドレター監視、およびコンシューマ冪等性は現在、設計の一部となっています。```text
Command commit
→ Outbox record
→ Relay publishes event
→ Projection consumes event
→ Read model updates
```このフローでは、厳密に 1 回の約束ではなく、少なくとも 1 回の配信とべき等処理を設計する方が現実的です。たとえば、投影はイベント ID を記録します。同じイベントが再度発生しても、ビューは再度変更されません。
## 単純なソリューションが壊れるのはなぜですか?
- ハンドラーからブローカーにメッセージを直接送信する: トランザクションのロールバック中にメッセージがすでに消費されている可能性があります。
- 各クエリの集計の読み込み: リスト画面は、ドメイン ルールと結合により不必要にコストがかかります。
- 読み取りモデルの遅延をエラーとして考慮する: 一部の画面では新しいデータが必要ですが、一部の画面では数秒の遅延が許容されます。この区別は製品によって決定される必要があります。
- 物理 CQRS に関するあらゆる問題を解決します。2 番目のデータ ストアは単なるテクノロジーではなく、永続的な運用上のオーバーヘッドを伴います。
## 意思決定チェックリスト
1. コマンドの成功条件とべき等性スイッチがオンになっていますか?
2. すべての不変条件は集約境界で保存されますか?
3. クエリはドメインではなく使用シナリオの DTO に従って形成されていますか?
4. 読み取りモデルの遅延に対する期待は製品言語で定義されていますか?
5. Projection が再構築されるとき、システムはどのように監視および検証されますか?
CQRS は、無制限に拡張できる自動チケットではありません。正しく使用すると、意思決定をパイプラインで行うことができます。読書をニーズに合ったモデルに変換します。このようにして、システムはより可視化され、変更に対する抵抗力も高まります。
次のセクションでは、このロジックは単一サービスの制限を超えて、分散システムにおけるイベント ブローカー、プロジェクション、ロールバック戦略を使用した CQRS を検証します。
## 読み取りモデルとは何ですか?
読み取りモデルは、書き込み側のドメイン モデルのコピーではありません。画面のニーズに合わせて用意されたビューです。```text
Orders (write model)
Id | CustomerId | Status | Lines | Rules
↓ Projection
OrderSummary (read model)
Id | CustomerName | Status | Total | BadgeColor
```## CDC 対送信ボックス
|基準 | CDC |送信ボックス |
| --- | --- | --- |
|ドメインイベントの意図 |間接的 |開く |
| DB スキーマの依存関係 |高 |下 |
|リプレイ |強い |強い |
|イベントコンテンツコントロール |限定 |フル |
|マイクロサービス通信 |状況に応じて |非常に手頃な価格 |
## 単純なソリューションが運用環境で機能しなくなるのはなぜですか?```text
Handler
→ DbContext.SaveChanges()
→ Message publish
```2 番目のステップが失敗すると、データが書き込まれ、イベントは失われます。または、トランザクションのロールバック中に、次の呼び出しによってすでに副作用が発生している可能性があります。```text
await emailService.Send(...)
```したがって、Outbox はイベントを同じトランザクション制限に永続的に変更して公開します。電子メールなどの副作用は、再試行耐性のあるコンシューマでコミット後に処理されます。
## 実際のリクエストはどのように流れるのでしょうか?```text
POST /orders
↓
Controller
↓
Mediator.Send()
↓
Validation → Authorization → Logging → Metrics → Transaction
↓
Handler
↓
Aggregate
↓
Repository + Outbox
↓
Commit
↓
Relay → Broker / Kafka
↓
Projection
↓
Read database
↓
GET /orders → Query handler → DTO → Frontend
```CQRS は自動スケールではありません。しかし、パイプラインの各ステップが存在する理由を説明できれば、システムに意識的な責任の分離が導入されます。説明できない場合は、まだ CQRS を使用する準備ができていない可能性があります。
## このフローについて知っておく必要があるのはなぜですか?
リクエストがどの層を通過するかがわかっている場合:
- コントローラーやハンドラーに無作為に Validation を書き込まないでください。
- ビジネス ルールを HTTP 層に置くのではなく、集約境界で保護します。
- 必要以上に早くトランザクションをオープンしない。
- ロギング、認証、および測定コードを使用してハンドラーを拡大しないでください。
- 横断的な懸念事項をパイプラインに移動します。
クエリ側の違いも同様に実用的です。```text
Sipariş detayı
→ Order aggregate
→ kurallar ve davranış için doğru model
Sipariş listesi
→ OrderSummaryDto
→ ekrana hızlı ve dar bir görünüm
```この違いを見ると、CQRS はフォルダー レイアウトではないことがわかります。これにより、あらゆる責任を適切な場所に置くための規律として使用できるようになります。
FAQ
よくある質問
ハンドラーとは何ですか?
単一のコマンドまたはクエリのアプリケーション ワークフローを実行します。
メディエーターとは何ですか?
これにより、コントローラーはハンドラーを直接知らなくても、正しいハンドラーにリクエストを送信できるようになります。
「CQRS (コマンド クエリ責任分離) パイプラインはどのように機能しますか? コマンドとクエリ フローの構造」では何がわかりますか?
CQRS リクエスト パイプラインとは何ですか? HTTP リクエストは、コントローラー、MediatR、パイプライン動作、ハンドラー、送信ボックス、読み取りモデルをどのように通過するのでしょうか?
学んだエンジニアリング原則
- CQRS は 2 つのデータベースではありません。それは、読むことと書くことは異なる責任であることを受け入れることです。
- パイプラインはハンドラーから反復的なチェックを削除します。ビジネス上の意思決定を目に見える形で残します。
- 物理的な分離は、遅延、再試行、およびデータの鮮度が製品の決定として設計されている場合にのみ価値があります。
続きを読む
続きを読む
シリーズの次のシリーズ
分散システムにおける CQRS (コマンド クエリ責任分離): イベント、ブローカー、プロジェクション
CQRS は分散システムでどのように機能しますか?ドメイン イベント、メッセージ ブローカー、プロジェクション、送信ボックス、冪等性、および最終的な整合性の決定をエンドツーエンドで検査します。
シリーズの次のシリーズ
CRUD (作成、読み取り、更新、削除) から CQRS (コマンド クエリ責任分離) まで: 問題はコードではなくモデルです
CQRS とは何ですか? CRUD と CQRS の違いは何ですか? CQRS はどのような場合に使用する必要がありますか?大規模システムでは単一モデルでは不十分な理由を説明するガイド。
同じシリーズ
本番環境における CQRS (コマンド クエリ責任分離): 一貫性、エラー、および回復戦略
CQRS は実稼働環境でどのように安全に機能しますか?一貫性の遅れ、イベントの重複、シーケンスの破損、投影の回復、再試行、DLQ および Saga 戦略。