--- name: cqrs-to-event-sourcing description: > CQRSの実装においてイベントソーシングが必然的に必要となる理由を論理的に説明する。 C側からQ側へのデータ同期問題(計算された値の同期不可、トリガーの限界、ポーリングの スケーラビリティ問題、ダブルコミット問題)を段階的に分析し、イベントを真のデータソースに する設計への到達過程を示す。CQRS導入検討、アーキテクチャ設計時に使用。 対象言語: 言語非依存。 トリガー:「CQRSにイベントソーシングは必要か」「C側とQ側の同期方法」 「CQRSでモデルを分ける必要はないのか」「リードモデルの更新方法」 「なぜイベントソーシングが必要か」「ダブルコミット問題」「CQRSの同期問題」 「CQRSはESなしでも動くか」といったCQRS/ES必然性関連リクエストで起動。 --- # CQRSはなぜEvent Sourcingになるのか CQRSを実装すると、C側からQ側への同期問題に直面し、イベントソーシングに至る。これはオプションではなく、実装上の必然である。 ## よくある誤解: 「CQRSはモデルを分ける必要がない」 この解釈は危険な誤読である。 正しい意味は「システムのうち、CQRS領域と非CQRS領域に分けることができ、CQRSを部分導入できる」ということ。モデルを分けなくてよいのは**非CQRS領域**であり、CQRS領域内ではコマンドモデルとクエリモデルの分割は必須。 ``` システム全体 ├── CQRS領域 → モデル分割は必須 └── 非CQRS領域 → 分割不要(従来のCRUDで十分) ``` **「CQRSはモデルを分割しなくてもいい」という解釈は、もはやCQRSではない。** ## C側とQ側のデータの違い CQRSにおいて、C側(コマンド)とQ側(クエリ)のデータは根本的に異なる。 ### 具体例: カートシステム **C側テーブル**(ドメインモデルの永続化に必要な最小データ): | カートテーブル | カートアイテムテーブル | |-------------|-------------------| | カートID (PK) | カートアイテムID (PK) | | 顧客アカウントID | カートID (FK) | | 上限予算金額 | 商品ID | | 作成日時 | 数量 | | | 作成日時 | **Q側テーブル**(表示・検索に必要なデータ): | カートテーブル | カートアイテムテーブル | |-------------|-------------------| | カートID (PK) | カートアイテムID (PK) | | 顧客アカウントID | 商品ID | | 顧客アカウント名 ★ | 商品名 ★ | | 上限予算金額 | 数量 | | **合計金額** ★ | **単価** ★ | | | **価格** ★ | ★の値は**C側のデータベースに存在しない**。ドメインオブジェクトの振る舞いによって計算される派生値である。 ```scala case class Cart(id: CartId, items: CartItems, ...) { // 合計金額はドメインオブジェクトの計算結果であり、DBに保存されない def totalPrice(priceResolver: ItemId => Price): Price = items.fold(Price.zero){ (t, item) => t + item.price(priceResolver) } } case class CartItem(id: CartItemId, itemId: ItemId, quantity: Quantity, ...) { // 単価は外部から提供され、価格は計算される def price(priceResolver: ItemId => Price): Price = priceResolver(itemId) * quantity } ``` ## 同期方法の段階的検討と限界 ### 方法1: トリガーによる同期 C側のテーブル更新時にSQLでQ側を書き込む。 **限界**: - 静的データ(顧客名等)の転送には有効 - **計算された値(★)は同期できない** - ドメインロジックの計算結果はDBに存在しない - 同じ計算ロジックのSQLを書くのは困難であり、ビジネスロジックの重複を生む - 苦肉の策として計算結果もC側に保存すると、リポジトリのインターフェースが歪む ```scala // ❌ リポジトリに計算ロジックの責務が漏れる trait CartRepository { def store(cart: Cart, priceResolver: ItemId => Price): Unit // priceResolverはリポジトリの責務ではない } ``` ### 方法2: ポーリングによる同期 プログラムでC側テーブルを読み込み、ドメインオブジェクトで計算後、Q側に書き込む。 **限界**: - 計算結果のQ側転送は可能 - **「いつ変更されたか」を検知できない** - 変更トリガーがない - 全集約をポーリングする必要があり、**スケーラビリティがない** - 大量の集約が存在する場合、実用的ではない **結論**: 最新状態を手に入れるにしても、**更新イベントが必要**。 ### 方法3: イベント通知キューの導入 変更時にイベントをキューに発行し、Q側更新プログラムがイベントを受信して同期する。 ``` C側リポジトリ → RDB(テーブルA) + メッセージキュー(更新イベント) ↓ リードモデル更新プロセス ↓ C側テーブルから集約再現 → Q側テーブル書き込み ``` **限界**: **ダブルコミット問題**が発生する。 - RDBとメッセージキューは異なるストレージ - 同一トランザクションに統合できない - RDBへの書き込み成功 + キューへの書き込み失敗(またはその逆)が起こりうる - 高いコストを払う可能性がある ## 必然的な到達点: イベントソーシング ダブルコミット問題を回避するには、**イベントを真のデータソースにする**。 ### 設計の転換 ``` 従来: C側DB(状態を保存) + メッセージキュー(通知用) → 2つのストレージへの書き込み = ダブルコミット問題 イベントソーシング: イベントストア(イベントが真のデータソース) → 1つのストレージへの書き込みのみ → C側の状態はイベントから導出 → Q側の状態もイベントから導出 ``` ### 結果 - C側のDBはRDBである必要がなくなる(イベントの追記とIDごとの読み込みが可能なシステムであれば何でもよい) - NoSQL(KVS)が適している場合が多い - DynamoDB Streamsなどでスケーラブルにイベント読み込みが可能 - **これがEvent Sourcing** ``` イベントストア(真のデータソース) │ ├──→ C側: イベントからドメイン状態を再構築 │ └──→ Q側: イベントからリードモデルを構築 (計算された値も含めて自由に構築可能) ``` ## CQRSシステムのほとんどがEvent Sourcingを採用する理由 Lightbend社の調査によると、CQRSシステムのほとんどがEvent Sourcingを採用している。 | 段階 | 方法 | 問題 | |------|------|------| | 1 | トリガー | 計算された値を同期できない | | 2 | ポーリング | 変更検知できない、スケールしない | | 3 | イベントキュー | ダブルコミット問題 | | **4** | **Event Sourcing** | **上記すべてを解決** | Event Sourcingはオプションではなく、CQRSを真剣に実装すると**必然的に到達する設計パターン**である。 ## 判断フロー ``` CQRSの導入を検討している ↓ C側とQ側のデータは同一か? ├─ YES → CQRSは不要。従来のCRUDで十分 └─ NO(Q側に計算値・結合データがある) ↓ C→Qの同期をどうするか? ├─ トリガー → 計算値は同期できない ├─ ポーリング → 変更検知・スケーラビリティの問題 ├─ イベントキュー → ダブルコミット問題 └─ Event Sourcing → すべて解決 ``` ## 関連スキルとの関係 | スキル | 関係 | |--------|------| | `cqrs-tradeoffs` | CQRSの一貫性・可用性・スケーラビリティ。本スキルは**ESが必要な理由** | | `aggregate-transaction-boundary` | トランザクション境界。本スキルは**C→Q同期**の問題 | | `cross-aggregate-constraints` | 集約間制約。本スキルとは別次元の問題 | ## レビューチェックリスト ### CQRS設計 - [ ] CQRS領域と非CQRS領域を明確に分離しているか - [ ] CQRS領域内でコマンドモデルとクエリモデルを分割しているか - [ ] 「CQRSだがモデルは分けない」という矛盾した設計になっていないか ### C→Q同期 - [ ] Q側に必要なデータの中に、C側DBに存在しない計算値があるか確認したか - [ ] 計算値がある場合、トリガーでは解決できないことを理解しているか - [ ] C→Q同期の仕組み(イベント駆動)が設計されているか - [ ] ダブルコミット問題を認識し、対処しているか ### Event Sourcing - [ ] イベントを真のデータソースとする設計か - [ ] C側の状態がイベントから導出可能か - [ ] Q側のリードモデルがイベントから構築可能か ## 関連スキル(併読推奨) このスキルを使用する際は、以下のスキルも併せて参照すること: - `cqrs-tradeoffs`: CQRS採用判断のトレードオフ分析 - `cqrs-aggregate-modeling`: イベントソーシング下での集約モデリング - `aggregate-transaction-boundary`: イベントストアによるダブルコミット問題の解消