/note/tech

CQRS/ESはなぜ書き込みDBと 読み取りDBを分けるのか?

要約:

■ 1. 発表の概要と問題提起

  • 発表者はRailsWayを守りつつDDD・モジュラーモノリス・クリーンアーキテクチャを実践し、さらなる設計スキルアップとしてCQRS/ESをフルスクラッチで学習中
  • 「CQRS/ESはソースコードレベルで実装を意識していればメリットを受けられるのか」という問いを出発点とする
  • 発表者の見解として、書き込みDBと読み取りDBが依存している場合はCQRS/ESのメリットを享受できない
  • 本LTの目的は、CQRS/ESにおいてなぜ書き込みDBと読み込みDBを分けるとよいのかを伝えること

■ 2. DBが依存している場合の問題

  • 典型的なコード例として、コマンド(更新処理)の直後にクエリ(取得処理)で同一テーブルを参照するパターンが挙げられる
  • 書き込みDB側の問題:
    • シンプルさと再現性を持たせることができない
  • 読み込みDB側の問題:
    • 拡張性が持ちにくく柔軟に対応できない

■ 3. CQRS/ESの書き込みDB・読み込みDBの特徴

  • 書き込みDB:
    • 「出来事」を追記するだけなので、いつ・何が起きたかを時系列で正確にさかのぼれる
    • 追記のみのシンプル構造により、従来のDB設計と比較して高トラフィック下でも高スループットを維持できる
  • 読み込みDB:
    • 画面やレポート用に「そのまま使える形」でデータを保持する
    • ユースケースごとに自由なリードモデルをいくつでも追加できる
    • データは自己完結であり、他のテーブルに依存しない

■ 4. CQRS/ESがもつ3つのメリットと、DBが依存する場合に失われる理由

  • システムで実行した履歴を追える:
    • 本来の姿: 追記のみのイベント蓄積データにより「いつ・誰が・何を」実行したかを保持できる、ログをリプレイすれば当時の挙動を再演できる、任意時点のRead Modelをいつでも即生成できる
    • 依存する場合に失われる理由: 状態を上書き保存する構造のためイベントが残らない、「最新の状態だけが真実」という前提に縛られ過去履歴が捨てられる、変更箇所がDBに残らないためログが存在するかどうかに依存するしかない
  • ハイトラフィックな書き込みへの対応がしやすい:
    • 本来の姿: 書き込みDBは最小限のデータ追記だけで済むため高速処理が可能であり、イベント追記のコードだけ調整すればパフォーマンスを改善できる
    • 依存する場合に失われる理由: 書き込みと読み込みを同じテーブルで連続実行するため書き込み処理が遅くなる、書き込み用と読み込み用が同一DBにあるため読み取り側の負荷も考慮しながらチューニングしなければならない、イベント追記とは無関係な箇所が遅延の原因になりうる
  • ユースケースに特化した表示用データが簡単に作成できる:
    • 本来の姿: ユースケースに合わせてRead Modelを任意の形で作成でき、余計なJOINや正規化を意識しなくてよい、UIや画面が変わっても書き込みロジックに影響しない
    • 依存する場合に失われる理由: イベントが更新方式では履歴が残らないため過去の流れをたどってRead Modelを再構築できない、読み取り用テーブルを追加するたびに書き込みDBのスキーマ変更が必要となり影響範囲が広く気軽に増やせない、普通のDB設計となりテーブル同士が独立せず複雑なSQLが必要になる

■ 5. 推奨される設計

  • 書き込みDBへの書き込みタイミング:
    • コマンドが実行されて確定し、イベントが発行されたタイミングで書き込みDBにイベントを追記する
    • 更新は行わず、イベントの追記のみを担い、表示側のことは一切考えない
  • 読み込みDBへの書き込みタイミング:
    • コマンド側からイベントの発行を受け取り、それをプロジェクティングして読み込みDBを更新する
    • 読み込みDBのリードモデルは、書き込みDBを参照して必要になったタイミングでいつでも作成できる
  • プロジェクティングとは:
    • イベントは何が起きたかという事実の記録であり、表示側の都合を考えず書き込みDBに追記する
    • そのイベントを表示用に整形して読み込みDBに書き込む処理がプロジェクティング

■ 6. CQRS/ESのDBの流れ

  • コマンドを実行 → イベント発行 → 書き込みDBにイベントを追記 → プロジェクティングによりリードモデルを生成 → 読み込みDBへ反映 → 表示用データの取得
  • 書き込みDBと読み込みDBは独立しており、読み込みDBは書き込みDBを参照して任意のタイミングで生成・再生成が可能

■ 7. DBを分けることが適しているユースケース

  • DBを分けることで複雑なシステムへの対応が可能になる
  • 適用が有効なケース:
    • ハイトラフィックなサービス
    • 業務向けサービスやSaaS
    • その他の複雑なサービス全般