CQRS
更新と参照を別のモデルにする
CQRS は、更新(コマンド)と参照(クエリ)で別のモデルを使う設計です。
通常は1つのモデルが両方を担います。同じ Order クラスで注文を確定し、同じクラスで一覧も表示します。CQRS ではこれを分けます。
更新側: OrderCommandHandler → 業務ルールを検証して状態を変える
参照側: OrderListView → 表示に必要な形へ整えたデータを返す
分けるのはモデルであって、必ずしもデータベースではありません。 同じDBのまま「更新用の型」と「参照用の型」を分けるだけでも CQRS です。
なぜ分けたくなるのか
更新と参照は、要求が食い違います。
| 更新側 | 参照側 | |
|---|---|---|
| 求めるもの | 業務ルールの一貫性 | 表示に必要な形と速さ |
| データの形 | 正規化された構造 | 画面ごとに結合済み |
| 頻度 | 少ない | 多い(数十倍〜) |
| 失敗の重さ | 重い(金額がずれる) | 軽い(再読み込みで済む) |
1つのモデルで両方を満たそうとすると、業務ルールを持つオブジェクトが表示用の項目を抱え込むか、表示のたびに複数テーブルを結合することになります。CQRS はこの綱引きを、分けることで解消します。
段階がある
CQRS は「全部やるか、やらないか」ではありません。
- 型だけ分ける — 同じDB、同じテーブル。更新用と参照用のクラスを分ける。ここだけで多くの問題が消える
- 参照用のクエリを分ける — 参照側はORMを介さず、表示に必要な列だけを直接引く
- 参照用のデータを別に持つ — 画面向けに整形済みのテーブル(読み取りモデル)を用意し、更新時に反映する
- データベースごと分ける — 更新用と参照用を別のストアにし、[[イベント駆動アーキテクチャ]] で同期する
3以降に進むと結果整合性が入ります。 更新した直後に参照すると、まだ古い値が見えることがあります。これを許容できる画面かどうかが、進むかどうかの判断基準です。
結果整合性との付き合い方
「保存したのに一覧に出ない」は、利用者から見れば不具合です。実務では次のように扱います。
- 更新直後だけ更新側から返す — 保存の応答に新しい値を含め、その画面だけは即座に反映する
- 反映待ちを明示する — 「反映まで数秒かかります」と示す
- 許容できない画面は分けない — 残高や在庫のように即時性が要る画面は、参照側も更新用データを見る
[[レプリケーション]] の読み取り専用レプリカにも同じ問題があります。 CQRS 固有の課題ではなく、読み書きを分けたときに常に付いてくる性質です。
使うべきか
- 効く — 参照の負荷が更新の何十倍もある。画面ごとに必要なデータの形が大きく違う。更新の業務ルールが複雑
- 過剰 — CRUD が中心。参照が単純な一覧と詳細だけ。チームが小さい
分ける対象は「システム全体」ではなく「一部の機能」で構いません。 注文だけ CQRS にして、マスタ管理は普通に書く、という使い分けが現実的です。[[ドメイン駆動設計(DDD)]] の境界づけられたコンテキスト単位で判断すると整理しやすくなります。
関連技術とのつながり
- [[イベント駆動アーキテクチャ]] — 更新側から参照側へ変更を伝える手段
- [[レプリケーション]] — 読み書きを分けたときの結果整合性という同じ課題を持つ
- [[マイクロサービス]] — サービス単位で読み書きの分離方針を変えられる
- [[リレーショナルデータベース]] — 正規化された更新側と、結合済みの参照側という対比
- [[ドメイン駆動設計(DDD)]] — どの範囲に適用するかの判断単位を与える
Q: CQRSで分けるものはどれ?
- [ ] 必ずデータベースを2つに分ける
- [x] 更新用と参照用のモデル(同じDBのままでも成立する)
- [ ] アプリケーションサーバーを分ける
解説: 型だけを分ける段階から始められます。DBを分けるのは最も進んだ段階です。
Q: 参照用のデータを別に持つ段階へ進むと発生する課題はどれ?
- [ ] 更新が遅くなる
- [x] 結果整合性 — 更新直後の参照で古い値が見えることがある
- [ ] 業務ルールが書けなくなる
解説: 即時性が要る画面かどうかが、この段階へ進むかの判断基準になります。
Q: CQRSが過剰になりやすいのはどんな場合?
- [ ] 参照の負荷が更新の何十倍もある場合
- [x] CRUDが中心で参照が単純な一覧と詳細だけの場合
- [ ] 画面ごとに必要なデータの形が大きく違う場合
解説: 分ける対象はシステム全体でなく一部の機能で構いません。使い分けが現実的です。