14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CQRSをめぐる二つの世界観

Last updated at Posted at 2025-12-16

貧血モデルの向こう側:失われた「意図」の言語

BEAR.Sunday Advent Calendar の続きです。前回の記事「Data Mapperをめぐる二つの世界観」では、MVCフレームワークにおける「モデルの空洞化(貧血モデル)」と、それを補うための「軽量DDD」という名の整理術について議論しました。

今回はその夜の続きです。koriymとたつきちは、居酒屋を出て、駅までの道を歩きながら、議論の続きを始めます。

⚠️ 重要: この議論の二人は実在の人物と一切関係がありません。ご注意ください。


ラウンド5:その整理は、どこまで効くのか

会場を出たあとの夜風は、熱気を帯びた議論のあとには心地よく感じられた。二人は駅へ向かう道すがら、先ほどの「ロジックの行き場」についての話を反芻していた。

たつきち:
「……でも、結局のところ、あれって“置き場所”の問題ですよね。」

koriym:
「というと?」

たつきち:
「MVCのMがデータモデルになって、ロジックが行き場を失った。だからServiceだのUseCaseだのを後付けして、結果として散らかった。だったら、最初から分ければいいんじゃないかって思うんです。読む処理と、書く処理を。 koriymさん、ご存知ですよね。"CQRS"、Qiitaでも時々見ます。」

たつきちは言い切った。整理としては、悪くないはずだという自信がある。

koriym:
「どんな感じにします?」

たつきち:
「読み込み用のRepositoryと、書き込みのRepositoryを分けて、書き込みはCommandとして投げる感じです。場合によってはCommand Busを挟んで……」

一瞬、言葉を選ぶ。

たつきち:
「正直、あれ、結構面倒ですけど。」

koriym:
「ええ。」

たつきち:
「DBを物理的に分けるとか、非同期メッセージングとか、Eventual Consistency(結果整合性)とか。図はきれいですけど、現場でやると運用が地獄ですよね。デバッグも大変だし、障害時に誰が責任を持つのかも曖昧になる。」

koriym:
「よく聞く話です。小規模なチームで導入して、オーバーエンジニアリングで自滅するパターンですね。ファウラーも、(本当に必要でない限りは)「CQRSの利用には慎重であるべきだ(you should be very cautious about using CQRS.)」って言ってますね。」

たつきち:
「ですよね。だから僕としては『概念としては分かるけど、実際は無理』っていう位置づけだったんです、CQRSって。それに、かのGreg Young (CQRS提唱者) も 『結局はReadとWriteを分けるだけだ(It’s just separating Read and Write)』みたいなことも言ってますよね。」

koriym:
「……言ってますね。」

たつきち:
「ですよね。だったら、ReadとWriteを分けて整理する、という理解で十分なんじゃないかと。」

koriymはすぐには返さなかった。ほんの一拍置いてから、静かに言った。

koriym:
「その『だけ』を、どう受け取るか、ですね。」

たつきち:
「……と言うと?」

koriym:
「Gregが言いたかったのは、『もっと複雑に考えろ』ではなく、『余計なものを足すな』ということです。Busも、分散も、イベントも、まずは全部忘れていい。それらはCQRSの本質ではありません。」

たつきち:
「そうなんですか……」

間が空く。

koriym:
「それでも残るものが、CQRSで中でも最も大事なもの..Command です。」

たつきちはちょっとキョトンとした。真意が掴めない。

たつきち:
「...Commandって、要は書き込みですよね、ぶっちゃけていえばDBのテーブル更新です。ユーザーや記事をテーブルにINSERTすることですよね。僕は(もちろん)Doctrine ORMでやりますが」

koriym:
「...そう見える、というだけです。」

たつきちは黙ったまま、考える。

koriym:
「じゃあ、こう考えてみましょう。『書き込みが失敗する』って、どういう意味だと思います?」


ディスカッションの分析

このフェーズでは、CQRSに対する認識のズレが焦点となりました。

  1. 「整理術」としてのCQRS(たつきちの視点):

    • たつきちはCQRSを「R/W分離によるコード整理」と捉え、それに付随するCommand Busや分散システムの複雑さを嫌っていました。これは多くのエンジニアが抱く「CQRS=オーバーエンジニアリング」という感覚を代弁しています。
  2. 「言語」としてのCQRS(koriymの視点):

    • koriymは、複雑な実装(BusやEvent)を削ぎ落とし、「Commandとは何か」という一点に絞りました。
  3. 「書き込み」という固定観念:

    • たつきちは「Command=結局は書き込み(Write)処理だろう」という感覚から抜け出せていません。koriymはそれを否定せず、次の章への問いかけにつなげます。

ラウンド6:名もなきif文

たつきち:
「書き込みが失敗する……?それは単純に、バリデーションエラーか、ユニーク制約違反、あるいはトランザクションのロールバックですよね。現場ではExceptionを投げるか、falseを返すだけです。」

koriym:
「技術的なエラーの話ではありません。私が聞きたいのは、『なぜ、その書き込みは拒絶されたのか』という理由です。」

たつきち:
「なぜって……。例えば『在庫がない』とか、『退会済みだから操作できない』とか、業務ルールに引っかかったからでしょう。」

koriym:
「そのルールは、どこに書いてありますか?」

たつきち:
「Serviceクラスです。$user->isDeactivated() をチェックして、真ならExceptionを投げます。そのあとで $repository->save() する。ごく普通のコードです。」

koriym:
「では、その 『チェックしてから save するまで』のブロックに、名前はついていますか?『退会済みかどうかを確認し、問題なければ更新する』という一連の判断と行動。それは、何という名前のメソッド、あるいはクラスになっていますか?」

たつきち:
「いや……名前というか……強いて言えば UserService::update メソッドの一部、ですね。汎用的な更新処理の中に、if文として埋まっています。」

koriym:
「...」

koriymは歩調を緩めずに続けた。

koriym:
「それが、『Commandが存在しない』という意味です。」

たつきち:
「……どういうことですか?」

koriym:
「たつきちさんは今、『更新(Write)』という大きな枠組みの中で考えています。だから、重要なビジネス判断が『ただのif文』として埋没している。CRUDの Update にマップしたせいで、『退会済みなら弾く』という業務の意図が、単なる条件分岐に格下げされてしまってるんです。」

たつきち:
「...いやいや、でも、コードとしてはそこに書いてあるわけだし、機能しますよ?」

koriym:
「機能はします。ですが、意図が見えない。では逆に聞きます。もしその処理が DeactivateUser (ユーザー停止)という独立したクラスだったらどうですか?クラスの責務は『ユーザーを停止すること』だけ。そのクラスは、実行されたらまず『すでに停止済みか』をチェックする。ダメなら、独自の例外UserAlreadyDeactivated を投げて失敗する。成功したら、停止フラグを立てて終わる。」

たつきち:
「まあ、それならスッキリはしますね。やることも、失敗する理由も明確ですから。」

koriym:
「それが Command です。」

たつきちは足を止めた。koriymが本題に入る。

koriym:
「データの書き込み(Write)は、最後に起きる結果にすぎません。Commandの本体は、その手前にある『やっていいかどうかの判断』です。汎用的な update メソッドの中に埋もれていた『名もなきif文』を切り出し、名前を与え、失敗の理由を明確にする。それが CQRS の C、つまり命令です。ビジネスルール、業務とも言っていい。」

たつきち:
「……ちょっと待ってください。じゃあ、僕が今まで『書き込み処理』だと思っていたものは……」

koriym:
「Command(命令)と、Write(永続化)が、ごちゃ混ぜになったものです。だから、それを『書き込み』としか呼べなかった。」

たつきち:
「……」

koriym:
「失敗の理由が『業務』であるなら、それは書き込み処理ではなく、意思決定、つまりコマンドなんです。」


ディスカッションの分析

  1. 「名もなきif文」の発見:

    • koriymは、「Write」という言葉が持つ「データを保存する」というイメージを剥がし、その前段にある「意思決定(判断)」に焦点を当てさせました。
    • MVCのService層で、重要なビジネスルールが汎用的な更新メソッドの中の if 文として埋没している現状を指摘しました。
  2. Commandの本質:

    • Writeは「結果(データの変更)」ですが、Commandは「意思決定(判断と命令)」です。
    • UpdateUser ではなく DeactivateUser と名付けることで、それは単なるDB操作ではなく、明確な意図を持ったオブジェクトとなることが示されました。

ラウンド7:二つのドア、一つの部屋

たつきち:
「……なるほど。Commandが『意思決定』であり、単なる書き込みではないことは分かりました。だとしたら、実装としてはこうなりますか?今まで一つの UserRepositoryfindsave も入っていたのを、UserReadRepositoryUserWriteRepository に物理的に分割する。Greg Youngも『ReadとWriteを分けるだけだ』と言っていたわけですし、リポジトリを分けるのがその第一歩ですよね?」

たつきちは、ようやく具体的なコードの形が見えてきた気がして、少し安心した表情を見せた。しかし、koriymは首を横に振った。

koriym:
「それが、最も陥りやすい罠です。
Gregが言った分ける(Segregation)は、『Read/Write Repository Segregation(リポジトリの分割)』ではありません。」

たつきち:
「えっ? リポジトリを分けることじゃないんですか?」

koriym:
「たつきちさん、想像してみてください。入り口(Repository)を読み取り用と書き込み用の二つに分けました。でも、その奥で扱っている『Userエンティティ』が、もし 同じクラス だったらどうなりますか?」

たつきち:
「……同じクラス?まあ、DoctrineのEntityは同じものを使いますよね。読み込む時も、書き込む時も。」

koriym:
「そう、それが問題なんです。
入り口のドアを二つに増やしても、中にある部屋(モデル)が一つなら、それはCQRSではありません。Gregの真意は、メディア(保存媒体)やリポジトリの区別ではなく、『モデル(関心ごと)の分離』なんです。考えてみてください。Command(書き込み側)のモデルに必要なのは何ですか?不変条件を守ること、矛盾した状態を拒否すること、つまり『振る舞い』です。ここにはゲッター(Getter)すら要らないかもしれない。」

たつきち:
「……確かに。判断するだけなら、内部状態が見える必要はないですね。」

koriym:
「一方で、Query(読み取り側)のモデルに必要なのは?画面に表示するための、整形されたデータです。結合(JOIN)され、加工され、使いやすく並べられた『構造』です。ここには複雑なビジネスロジックも、セッター(Setter)も要らない。」

たつきち:
「……言われてみれば、真逆ですね。片方は『ガードが堅いブラックボックス』で、もう片方は『明け透けなデータ構造』だ。」

koriym:
「その通りです。それなのに、私たちは『Userエンティティ』というたった一つのクラスに、その両方の責務を負わせてきました。だからエンティティは、バリデーションロジックと、表示用のゲッターで膨れ上がり、複雑怪奇になる。
Gregが『分けるだけだ』と言ったのは、『それら二つは全く別の生き物なのだから、無理に一つのクラスで表現するな』という、極めてシンプルな提言だったんです。」

たつきち:
「うわあ……。僕、完全に誤解していました。『リポジトリを分けて整理整頓する話』だと思っていたけど、実際は 『一つのモデルで全てを解決しようとするな』 という、モデリングの話だったんですね。」

koriym:
「ええ。R/W分離の本質は、『最適化の方向が違う二つのモデルを、独立させること』です。Repositoryを分けるのは結果であって、目的ではありません。」


ディスカッションの分析

  1. 「リポジトリ分割」というミスリードの解消:

    • 「Repositoryを分ける=CQRS」という、形だけ真似て本質(モデルの分離)を逃す典型的な失敗例が指摘されました。
  2. 「同じ部屋」の比喩:

    • 「ドア(Repository)を二つにしても、部屋(Entity)が同じなら意味がない」という比喩で、MVCフレームワークにおける「万能エンティティ(God Class)」の問題点が浮き彫りになりました。

ラウンド8:使い捨ての美学

たつきち:

「モデルを分ける……。Command側が『判断のためのモデル』だというのは分かりました。でも、Query(読み取り)側はどうなるんですか?今まで通り、DoctrineのEntityを使ってデータを取得するんじゃダメなんですか?」

koriym:
「CommandとQueryは、求められる要件が正反対なんです。Commandは正規化され、整合性が保証された『正しい状態』が必要です。一方で、Queryに必要なのは何だと思いますか?」

たつきち:
「それは……画面に必要なデータ、ですよね。」

koriym:
「そうです。画面に表示するための、整形されたデータです。そこには、テーブルの正規化なんて関係ない。複数のテーブルがJOINされ、合計値が計算され、使いやすく並べられた 『構造(Structure)』があればいい。そこには、複雑なビジネスロジックも、バリデーションも、セッター(Setter)さえも要りません。」

たつきち:
「セッターも要らない……。まあ、表示するだけですからね。でも、そのためだけに専用のクラスを作るんですか?せっかく User エンティティがあるのに、似たような UserDto みたいなものを作るのは、DRY原則に反するような……。」

たつきちは、「同じようなクラスが増える」ことへの生理的な拒否感を示した。しかし、koriymは楽しそうに笑って言った。

koriym:
「たつきちさん、Greg YoungはQuery側のモデルについて、なんと言っているか知っていますか? Queryモデルは使い捨て(Disposable)でいい』と言っているんです。」

たつきち:
「つ、使い捨て……!?」

koriym:
「ええ。Queryモデルは、ドメインの真理を表すものではありません。『ある瞬間の、ある画面の都合』に合わせて切り取られた、ただのスナップショットです。だから、永続的にメンテナンスする高貴な『ドメインモデル』である必要はない。画面が変われば、捨てて作り直せばいい。Gregはそう言ってます。」

たつきち:
「!! 」

koriym:
「私たちは今まで、その『使い捨てでいいはずのデータ』を取得するために、重厚なDoctrineエンティティを使い、リレーションを張り、Lazy Loadingに怯え、シリアライザーで循環参照と戦ってきました。でも、Queryを分離するなら、そんな苦労は一切不要になる。必要なカラムだけをSELECTした、単純なクラス(DTO)があればいいんです。」

たつきち:
「……なんか、力が抜けました。僕は『正しいモデルを作らなきゃ』と必死でしたが、読み取り側に関しては、もっと 『適当(割り切り)』 でよかったんですね。」

koriym:
「適当、ではありません。『最適』なんです。読み取りに特化した形に最適化すること。それを許容するのが、CQRSのもう一つの自由です。」


ディスカッションの分析

  1. 「使い捨て(Disposable)」の衝撃:

    • 「Entity=唯一の正解」という固定観念を持っていたたつきちに、Greg Youngの「Queryモデルは使い捨てでいい」という言葉をぶつけることで、「データモデルの神聖化」 を解体しました。
  2. Commandとの対比:

    • Commandモデル=高凝集・厳格(判断)
    • Queryモデル=使い捨て・柔軟(表示)
    • この対比により、「なぜ分けるのか」の理由が、整理整頓ではなく 「それぞれの責務に特化させるため」 であることが明確になりました。

ラウンド9:失われた言葉たち

たつきち:
「……あのー、一つ気づいてしまったんですけど。」

koriym:
「なんでしょう?」

たつきち:
「Commandは『意図』、Queryは『構造』。Gregはそう言った。でも、僕らの周りを見てください。MVCのモデル、REST API、Scaffold。これ全部、『意図』も『構造』も、十把一絡げにしてCRUDに押し込んでませんか?

koriymが静かに頷く。

たつきち:
「なんでもかんでもCRUDでやろうとするから、うまくいかないんじゃないですか。本来は『業務の遂行』という意図を表すべきところを、単なる『データ操作』としてしか捉えていない。でも、それって結局、MVCフレームワークのMが『データ操作』になってしまっているせいでもありますよね。当たり前すぎて疑問にも思わなかったですけど」

koriym:
「ええ。モデルがいつの間にか『データベースの代理人』になってしまった。ユーザーは『行をUPDATEしたい』わけじゃない。『引越しをしたい』んです。でも、フレームワークが用意したMが『住所カラムの書き換え』しか表現できないなら、コードもそれに従うしかない。」

たつきち:
「だからFat Controllerになるし、ロジックも散らかるわけか……。僕はずっと『Doctrineの使い方』の問題だと思ってました。でも違った。CRUDという枠組みだけで、複雑なビジネスを表現しようとしたこと自体に、そもそも無理があったんですね。」

koriym:
「CRUDの引力は強いですからね。それだけ利便性をもたらしてきたのも事実です。しかし意識して『意図』をコードに残さないと、すべてはただのデータ更新に飲み込まれてしまいます。」

夜の冷気の中、二人の足音だけが響いた。

特定のライブラリがどうとか、アーキテクチャがどうとか、そんな話はもう出てこなかった。ただ、「なぜ自分たちの開発はいつも辛いのか」という問いの答えだけが、重く、しかし確かにそこに横たわっていた。

(完)

image.png

編集後記

前回の「貧血モデル」の議論が「現状の肯定(テスト容易性)」で幕を閉じたのに対し、今回のCQRS編は、その足元にある「構造的な絶望」に触れる展開となりました。

多くのエンジニアがCQRSという言葉を聞いたとき、反射的に「マイクロサービス」「イベントソーシング」「結果整合性」といった“複雑な実装詳細”を思い浮かべます。劇中のたつきちもそうでした。しかし、提唱者であるGreg Youngが伝えたかった本質は、もっとシンプルで、かつ残酷なまでに根源的な問いです。

あなたはビジネスの意図(Command)を語っているか? それとも、ただデータベースの更新(CRUD)を語っているか?

CQRSは単なるデータモデルの分離ではありません。また今回の対話で描きたかったのは、特定のアーキテクチャの優劣ではありません。現代のWeb開発において、MVCフレームワークやORMといった便利な道具が、いかにして私たちの思考から 「動詞(意図)」を奪い、無機質な「データ操作」へと均質化させているか、という事実です。

「コントローラーが太る」「ロジックが散らかる」といった現場の悩みは、エンジニアのスキル不足だけが原因ではありません。そもそも 「意図を受け止める場所」が用意されていない構造の上で、必死に意図を表現しようとするから——しかもCRUDという思考フレームワークとその道具を手放すことなく——歪みが生まれるのです。この構造に気づかない限り、終わらない「ついに辿り着いたアーキテクチャの正解」の話は、また来年も繰り返されるのではないでしょうか。

CQRS

CQRSの本質的洞察については以下の2つの記事もご覧ください。

この2つの記事の洞察は素晴らしく、以前 Greg Young 氏に査読を依頼した際も、そのCQRS理解が適切なものであるとお墨付きをいただいています。

これらで紹介されている task-based-ui の理解こそが、CQRS理解の鍵です。中でもNaokiTsuchiyaさんが紹介しているGregのRESTとCQRSに関するツイートは、彼が考えるCQRSの本質をよく表しており、この記事の主題とも関連します。

(Gregは当初、RESTを「単なる CRUD over HTTP のようなもの」だと誤解していたのですが1、後にRESTをHATEOASの文脈で再理解しました。そして、RESTの本来の姿(ハイパーメディア駆動)が、CQRSの理想とする「振る舞いの分離」と完全に合致することに気づき、前言を撤回・修正したのです。)

CQRSは魅力的な考え方ですが、どのように実現するかの How ばかりが語られ、肝心の本質、それがなんであるかの What が置き去りにされたり、誤解されているように感じることがあります。(前回の記事で取り上げたMVCのモデル(そしてDDD)もまた、同じ文脈です。)この記事が、アーキテクチャのWhatを問う姿勢に少しでも繋がれば幸いです。

次はいよいよ完結編へと続きます!

  1. RESTもCQRSと同じように広く誤解されていますが。

14
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?