CQRS
CQRS ・ しーきゅーあーるえす
書き込み(コマンド)と読み取り(クエリ)のモデルを分け、それぞれを独立に最適化する設計。
✦ 30秒で試す — 航海書く係と読む係を分けたら、どちらも速くなった注文した直後に履歴を開くと、自分の注文は?予想してみる概要
CQRS(Command Query Responsibility Segregation、コマンドクエリ責務分離)は、データを「書き込む(コマンド)」処理と「読み取る(クエリ)」処理を、別々のモデルとして分離する設計パターンです。多くのシステムは1つのデータモデルで読み書きの両方をこなしますが、CQRS はそこをあえて2つに割り、書き込み用の構造と読み取り用の構造をそれぞれ独立に最適化します。
ふつうの CRUD 的な設計では、同じテーブル・同じオブジェクトに対して、更新もするし一覧表示のための検索もします。ところが「正しく整合を保って書きたい」という都合と、「速く・画面に都合のよい形で読みたい」という都合はしばしば衝突します。CQRS は、その2つの要求を1つのモデルに背負わせるのをやめ、書き込み側は業務ルールと整合性に集中し、読み取り側は表示や検索のしやすさに集中させる、という分業を持ち込みます。代償として、書いた内容が読み取り側に現れるまでに時間差が生まれ、システムは結果整合性で動くことになります。
なぜ生まれたか
伝統的なアプリケーションは、1つのドメインモデル(データの構造とふるまいをまとめたもの)で読み書きの両方をこなしてきました。小規模なうちはこれで十分ですが、システムが育つと読み書きの要求がちぐはぐになってきます。書き込みは「在庫を二重に引き当てない」といった不変条件を厳密に守る必要がある一方、読み取りは「商品一覧を、店舗名やカテゴリと結合して、並べ替えて高速に返す」ことを求めます。
この2つを1つのモデル・1つのテーブル構成で満たそうとすると無理が出ます。正規化して整合を守りやすくすると、読み取りのたびに多数の結合が必要になって遅くなる。逆に読み取りを速くするために非正規化すると、今度は書き込み時の整合維持が難しくなる。読み書きは負荷の量も性質もまったく違うのに、同じモデルに縛られているせいで、片方を最適化するともう片方が犠牲になるのです。
発想の源は、Bertrand Meyer が1988年の著書(邦訳『オブジェクト指向入門』)で示した CQS(Command-Query Separation)という原則にあります。メソッドは「状態を変えるコマンド」か「値を返すだけのクエリ」のどちらかであるべきで、両方を兼ねてはならない、というものです。Greg Young は2010年ごろ、この区別をメソッド単位からアーキテクチャ単位へ広げ、「読み書きを同じモデルで扱っていること」自体が綱引きの原因だとして、両者のモデルとデータストアを丸ごと切り離す CQRS を提唱しました。書き込み側と読み取り側を別モデルにすれば、それぞれを独立に最適化し、別々にスケーリングできます。読み取りが圧倒的に多いサービスなら、読み取り側だけを増強できるわけです。
詳細
コマンド側とクエリ側を分ける
CQRS では、アプリケーションへの操作を2種類に分けて考えます。コマンドは「注文を確定する」「住所を変更する」のように状態を変える操作で、業務ルールを検証したうえで書き込みモデルに反映し、結果として返すのは成否程度です。クエリは「注文履歴を表示する」のように状態を読むだけの操作で、何も変えません。この「状態を変えるか、読むだけか」で処理経路そのものを分けるのが CQRS の骨格です。
次の図は、書き込みと読み取りを別のモデル・別のデータストアに分け、両者を結果整合性でつなぐ典型的な構成です。
図のように、クライアントの操作はコマンドとクエリに振り分けられ、それぞれ専用のモデルとデータストアへ向かいます。書き込みが起きると、その変化はイベントとして流れ、投影器(プロジェクタ)と呼ばれる処理がリードモデルへ反映します。読み取り側は「表示したい形そのまま」にデータを持てるので、結合や整形をせずに高速に返せます。一覧用、集計用、全文検索用というように、リードモデルは用途ごとにいくつあってもかまいません。同じイベントを別々の投影器が購読し、それぞれの形に組み立てます。事前に結合・集計した結果を持つという点では、データベースのマテリアライズドビューをアプリケーションの側で作っているとも言えます。
書いた直後に読むと — 結果整合性の「窓」
同期の多くは非同期で行われるため、コマンドが成功してから投影がリードモデルに届くまでの間は、読み取り側が古いままです。ユーザーが注文ボタンを押した直後に一覧を開くと、いま入れたはずの注文が載っていない — これが CQRS でいちばん最初にぶつかる違和感です。下の部品で、注文した直後に一覧を読み、何が返るかを確かめてください。
左の「注文する」を押し、粒が右下の一覧に届く前に「一覧を読む」を押してください。
この窓への対処はいくつかあります。一つは、書いた本人にだけ自分の書き込みが見えることを保証する read-your-writes です。コマンドの応答で「書いた版(バージョン)」をクライアントに返し、次のクエリでは「その版以上」を条件にして、リードモデルが追いつくまで待つか、追いついていなければ書き込み側から読みます。もう一つは画面の側での工夫で、送信した内容をいったん手元で一覧に差し込んでおく(楽観的な UI 更新)方法です。どちらも「他人の書き込みの遅れ」までは消せないので、業務としてその遅れを許容できるかが採否の分かれ目になります。
リードモデルは作り直せる
リードモデルは書き込み側から導かれる「写し」にすぎないので、壊れたり形を変えたくなったりしたら、捨てて作り直せます。書き込み側に変更の履歴(イベントの列)が残っていれば、それを先頭から投影器に流し直すだけです。新しい画面のために新しいリードモデルを足すのも同じ手順で、過去のイベントを流せば最初から埋まった状態で使い始められます。逆に言えば、履歴を残さずに現在値だけを持つ構成では、作り直しのたびに書き込み DB から全件を読み出す別の仕組みが要ります。
イベントソーシングとの組み合わせ
CQRS がもっとも自然に噛み合うのがイベントソーシングとの組み合わせです。イベントソーシングは状態変化をイベント列として追記しますが、そのイベント列は「何が起きたか」の記録には理想的でも、一覧検索には向きません。コマンド側はイベントをイベントストアに追記するだけに徹し、そのイベントを購読して、検索・表示に最適化したリードモデル(投影、プロジェクション)を組み立てておく — こうすると、書き込みは追記専用で速く、読み取りは専用の形で速い、という両取りが成立します。
ただし両者は必ずセットというわけではありません。CQRS は「読み書きのモデルを分ける」という考え方であって、その裏側がイベントソーシングである必要はなく、ふつうの RDBMS の中で書き込み用テーブルと読み取り用テーブル(あるいは読み取り専用のレプリケーション先)を分けるだけの軽い CQRS も十分に成立します。逆にイベントソーシングを採るなら、読み取りのために CQRS はほぼ必須になります。この非対称な関係を押さえておくと、両者を混同せずに済みます。
トレードオフと使いどころ
CQRS の代償は複雑さです。モデルもデータストアも2系統以上に増え、投影器という同期の仕組みを作り、止まっていないか・どれだけ遅れているかを監視する必要があります。投影器が同じイベントを2回受け取っても壊れないよう冪等に作ることも欠かせません。
したがって CQRS は、あらゆる場面で使うべき万能パターンではありません。読み取りと書き込みの負荷や要求が大きく食い違う領域、複雑な業務ルールを持つドメイン、あるいはイベントソーシングを採用する部分にこそ効きます。単純な CRUD にまで持ち込むと、得られる恩恵よりも二重管理のコストのほうが上回ります。システム全体に一律で適用するのではなく、ドメイン駆動設計でいう境界づけられたコンテキストのうち、恩恵の大きい一部にだけ選択的に導入するのが実務での定石です。
試問
通販の注文を、書き込みDB(コマンドが書く側)と、注文一覧のリードモデル(クエリが読む側)に分けています。注文のイベントは投影器を通って、少し遅れて一覧に届きます。「注文する」「一覧を読む」のボタンと、クエリの読み方(そのまま読む/自分の書き込みを待つ)を使えます。投影器は押すと止まります。
左の「注文する」を押し、粒が右下の一覧に届く前に「一覧を読む」を押してください。
