From cb2846a080d2393fbc08ca14e93e6c9949b38d14 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sat, 6 Jun 2026 02:14:45 +0900 Subject: [PATCH 1/4] Clarify CQRS guidance in BDR FAQ --- BDR_PATTERN-ja.md | 45 ++++++++++++++++++------------------------ BDR_PATTERN.md | 45 ++++++++++++++++++------------------------ docs/bdr-pattern-ja.md | 45 ++++++++++++++++++------------------------ docs/bdr-pattern.md | 45 ++++++++++++++++++------------------------ 4 files changed, 76 insertions(+), 104 deletions(-) diff --git a/BDR_PATTERN-ja.md b/BDR_PATTERN-ja.md index 4822b6b..b246a0d 100644 --- a/BDR_PATTERN-ja.md +++ b/BDR_PATTERN-ja.md @@ -612,50 +612,43 @@ BDRパターンでは、それぞれが自身の領域で優秀さを発揮し ### Q: 変更されたオブジェクトをどうやってDBに書き戻す(保存する)のですか? -**A: 書き戻しません。** BDRパターンのオブジェクトは読み取り専用であり、データのクエリのために存在します。データを変更する必要がある場合: +**A: BDR のクエリオブジェクト自体は保存しません。** BDR パターンのオブジェクトは読み取り専用の Query モデル、つまり画面、帳票、API レスポンス、ユースケースに合わせた projection です。状態を変更する必要がある場合は、その変更を Command として表します。 -1. **アプリケーション層でビジネス判断を行う** -2. **コマンドを発行する** - 明確で明示的な書き込み操作 -3. **シンプルな書き込みクエリを実行** - UPDATE、INSERT、DELETE文 +1. **意図に名前を付ける** - `ProcessOrder`、`DeactivateUser`、`ChangeShippingAddress` +2. **Command 側で判断する** - 不変条件を守り、成功/失敗の理由を明確にする +3. **結果を永続化する** - 必要な UPDATE、INSERT、DELETE を Command の流れで実行する これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の原則に従います: ```php -// クエリ側(BDRパターン) -$order = $this->orderRepo->getOrder($id); -if ($order->canProcess()) { - // コマンド側(シンプルな書き込み) - $this->orderCommandRepo->markAsProcessed($id, new DateTime()); +// Query側(BDRパターン): 今の用途に必要なprojection +$order = $this->orderQuery->getOrder($id); +if ($order->canShowProcessAction()) { + // アプリケーションは操作を提示できる。ただし最終判断はCommand側が行う。 + $this->processOrder->execute($id, new DateTimeImmutable()); } -// orderCommandRepoはシンプルなSQLを使用: +// processOrder は必要に応じて明示的な書き込みSQLを使う: // UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id ``` この分離は意図的です: -- **クエリ**は複雑で、JOINや集約を含むことができる -- **コマンド**はシンプルで、状態変更に集中すべき -- **ドメインロジック**はクエリオブジェクトに存在し、データベース書き込みには存在しない +- **Query** は JOIN、集約、計算、projection SQL で用途に合わせて形を作れる +- **Query オブジェクト**は今読むための読み取り専用構造で、用途が変われば置き換えてよい +- **Command** は業務の意図と不変条件を表し、書き込みはその判断を永続化した結果である ### Q: これはCQRSパターンですか? -**A: はい、特にクエリ(読み取り)側です。** BDRパターンはCQRSのクエリ側の強力な実装です。 +**A: はい。BDR は CQRS の Query 側を表現します。** ただし、それは Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。 -CQRSは読み取りと書き込みの責務を分離します: -- **クエリ側(BDRパターン)**:ビジネスロジックを含む豊富なドメインオブジェクトによる複雑な読み取り -- **コマンド側**:状態を変更するシンプルで焦点を絞った書き込み +CQRS は、データソースや Repository の置き場所のパターンとして説明されることがあります。それは図にしやすい形ですが、本質ではありません。本質は、Command と Query が最適化する関心が違うため、それらを一つの Repository や Entity モデルに押し込まないことです。 -BDRパターンは複雑な部分(クエリ)を以下を組み合わせて処理します: -- データ取得のためのSQLの力 -- 変換と充実化のためのファクトリー -- ビジネスロジックのためのドメインオブジェクト +- **Command 側**:意図、振る舞い、不変条件、失敗理由を表す。「この行為は実行してよいか?」に答える +- **Query 側(BDR パターン)**:今の読み手に必要な projection、構造、並びを表す。「今読むにはどんな形が役に立つか?」に答える -一方、コマンド側はシンプルに保たれます: -- 直接的なUPDATE/INSERT/DELETE文 -- イベントソーシング(必要な場合) -- 書き込み前のシンプルな検証 +SQL はそもそもこの Query 側の性質を持っています。`SELECT` は JOIN、集約、計算を使って、必要な形へ結果を projection できます。その構造を永続的な正規ドメインモデルであるかのように扱う必要はありません。BDR では、SQL ファイルがその projection を定義し、ファクトリやドメインオブジェクトが PHP の型として表面を与えます。 -この分離により、両側がよりシンプルで保守しやすくなります。 +Query モデルは使い捨てであってもかまいません。画面、帳票、API レスポンスが変われば、別の `SELECT` と小さな読み取りモデルを作ればよい。それは DRY 違反ではなく、CQRS の要点です。関心が違うものには、違うモデルを与えます。 ### Q: ファクトリーで外部APIを呼ぶと、リスト取得時に遅くなりませんか? diff --git a/BDR_PATTERN.md b/BDR_PATTERN.md index f0dacfe..aa893f3 100644 --- a/BDR_PATTERN.md +++ b/BDR_PATTERN.md @@ -564,50 +564,43 @@ In the BDR Pattern, each excels in its own domain while building something great ### Q: How do I save modified objects back to the database? -**A: You don't.** Objects in the BDR Pattern are read-only and exist for querying data. When you need to modify data: +**A: You don't save the query object itself.** Objects in the BDR Pattern are read-only Query models: projections shaped for a screen, report, API response, or use case. When state must change, model that change as a Command. -1. **Make business decisions** in your application layer -2. **Issue a Command** - a clear, explicit write operation -3. **Execute simple write queries** - UPDATE, INSERT, DELETE statements +1. **Name the intent** - `ProcessOrder`, `DeactivateUser`, `ChangeShippingAddress` +2. **Let the Command side decide** - enforce invariants and make success/failure reasons explicit +3. **Persist the result** - execute the necessary UPDATE, INSERT, or DELETE in the command flow This follows the **CQRS (Command Query Responsibility Segregation)** principle: ```php -// Query side (BDR Pattern) -$order = $this->orderRepo->getOrder($id); -if ($order->canProcess()) { - // Command side (simple write) - $this->orderCommandRepo->markAsProcessed($id, new DateTime()); +// Query side (BDR Pattern): projection for the current use +$order = $this->orderQuery->getOrder($id); +if ($order->canShowProcessAction()) { + // The application may offer the action, but the Command owns the final decision. + $this->processOrder->execute($id, new DateTimeImmutable()); } -// orderCommandRepo might use simple SQL: +// processOrder may use explicit write SQL: // UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id ``` The separation is intentional: -- **Queries** can be complex, with JOINs and aggregations -- **Commands** should be simple and focused on changing state -- **Domain logic** lives in the query objects, not in the database writes +- **Queries** can use JOINs, aggregations, calculations, and projection SQL to shape data for a specific use +- **Query objects** are read-only structures for the current use and can be replaced when that use changes +- **Commands** carry business intent and invariants; writes are the persistence result of that decision ### Q: Is this the CQRS pattern? -**A: Yes, specifically the Query (read) side.** The BDR Pattern is a powerful implementation of CQRS's query side. +**A: Yes. BDR expresses the Query side of CQRS.** But this is not mainly about placing read repositories and write repositories in different locations. It is about separating concerns and models. -CQRS separates read and write responsibilities: -- **Query side (BDR Pattern)**: Complex reads with rich domain objects containing business logic -- **Command side**: Simple, focused writes that change state +CQRS is often introduced as a data-source or Repository placement pattern. That is an easy shape to draw, but it is not the essence. The essence is that Command and Query optimize for different concerns, so they should not be forced into one Repository or Entity model. -The BDR Pattern handles the complex part (queries) by combining: -- SQL's power for data retrieval -- Factories for transformation and enrichment -- Domain objects for business logic +- **Command side**: intent, behavior, invariants, and failure reasons. It answers, "May this action happen?" +- **Query side (BDR Pattern)**: projection, structure, and arrangement for the current reader. It answers, "What shape is useful to read now?" -Meanwhile, the command side remains straightforward: -- Direct UPDATE/INSERT/DELETE statements -- Event sourcing (if needed) -- Simple validation before writes +SQL already has this Query-side character. A `SELECT` can join, aggregate, calculate, and project a result into the exact structure needed without pretending that structure is the canonical domain model. In BDR, the SQL file defines that projection, and the factory/domain object gives it a typed PHP surface. -This separation makes both sides simpler and more maintainable. +The Query model may be disposable. If a screen, report, or API response changes, write another `SELECT` and another small read model. That is not a DRY violation; it is the point of CQRS: different concerns get different models. ### Q: Won't calling external APIs in factories slow down list retrievals? diff --git a/docs/bdr-pattern-ja.md b/docs/bdr-pattern-ja.md index 32a8899..4fd2ea9 100644 --- a/docs/bdr-pattern-ja.md +++ b/docs/bdr-pattern-ja.md @@ -622,50 +622,43 @@ BDRパターンでは、それぞれが自身の領域で優秀さを発揮し ### Q: 変更されたオブジェクトをどうやってDBに書き戻す(保存する)のですか? -**A: 書き戻しません。** BDRパターンのオブジェクトは読み取り専用であり、データのクエリのために存在します。データを変更する必要がある場合: +**A: BDR のクエリオブジェクト自体は保存しません。** BDR パターンのオブジェクトは読み取り専用の Query モデル、つまり画面、帳票、API レスポンス、ユースケースに合わせた projection です。状態を変更する必要がある場合は、その変更を Command として表します。 -1. **アプリケーション層でビジネス判断を行う** -2. **コマンドを発行する** - 明確で明示的な書き込み操作 -3. **シンプルな書き込みクエリを実行** - UPDATE、INSERT、DELETE文 +1. **意図に名前を付ける** - `ProcessOrder`、`DeactivateUser`、`ChangeShippingAddress` +2. **Command 側で判断する** - 不変条件を守り、成功/失敗の理由を明確にする +3. **結果を永続化する** - 必要な UPDATE、INSERT、DELETE を Command の流れで実行する これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の原則に従います: ```php -// クエリ側(BDRパターン) -$order = $this->orderRepo->getOrder($id); -if ($order->canProcess()) { - // コマンド側(シンプルな書き込み) - $this->orderCommandRepo->markAsProcessed($id, new DateTime()); +// Query側(BDRパターン): 今の用途に必要なprojection +$order = $this->orderQuery->getOrder($id); +if ($order->canShowProcessAction()) { + // アプリケーションは操作を提示できる。ただし最終判断はCommand側が行う。 + $this->processOrder->execute($id, new DateTimeImmutable()); } -// orderCommandRepoはシンプルなSQLを使用: +// processOrder は必要に応じて明示的な書き込みSQLを使う: // UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id ``` この分離は意図的です: -- **クエリ**は複雑で、JOINや集約を含むことができる -- **コマンド**はシンプルで、状態変更に集中すべき -- **ドメインロジック**はクエリオブジェクトに存在し、データベース書き込みには存在しない +- **Query** は JOIN、集約、計算、projection SQL で用途に合わせて形を作れる +- **Query オブジェクト**は今読むための読み取り専用構造で、用途が変われば置き換えてよい +- **Command** は業務の意図と不変条件を表し、書き込みはその判断を永続化した結果である ### Q: これはCQRSパターンですか? -**A: はい、特にクエリ(読み取り)側です。** BDRパターンはCQRSのクエリ側の強力な実装です。 +**A: はい。BDR は CQRS の Query 側を表現します。** ただし、それは Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。 -CQRSは読み取りと書き込みの責務を分離します: -- **クエリ側(BDRパターン)**:ビジネスロジックを含む豊富なドメインオブジェクトによる複雑な読み取り -- **コマンド側**:状態を変更するシンプルで焦点を絞った書き込み +CQRS は、データソースや Repository の置き場所のパターンとして説明されることがあります。それは図にしやすい形ですが、本質ではありません。本質は、Command と Query が最適化する関心が違うため、それらを一つの Repository や Entity モデルに押し込まないことです。 -BDRパターンは複雑な部分(クエリ)を以下を組み合わせて処理します: -- データ取得のためのSQLの力 -- 変換と充実化のためのファクトリー -- ビジネスロジックのためのドメインオブジェクト +- **Command 側**:意図、振る舞い、不変条件、失敗理由を表す。「この行為は実行してよいか?」に答える +- **Query 側(BDR パターン)**:今の読み手に必要な projection、構造、並びを表す。「今読むにはどんな形が役に立つか?」に答える -一方、コマンド側はシンプルに保たれます: -- 直接的なUPDATE/INSERT/DELETE文 -- イベントソーシング(必要な場合) -- 書き込み前のシンプルな検証 +SQL はそもそもこの Query 側の性質を持っています。`SELECT` は JOIN、集約、計算を使って、必要な形へ結果を projection できます。その構造を永続的な正規ドメインモデルであるかのように扱う必要はありません。BDR では、SQL ファイルがその projection を定義し、ファクトリやドメインオブジェクトが PHP の型として表面を与えます。 -この分離により、両側がよりシンプルで保守しやすくなります。 +Query モデルは使い捨てであってもかまいません。画面、帳票、API レスポンスが変われば、別の `SELECT` と小さな読み取りモデルを作ればよい。それは DRY 違反ではなく、CQRS の要点です。関心が違うものには、違うモデルを与えます。 ### Q: ファクトリーで外部APIを呼ぶと、リスト取得時に遅くなりませんか? diff --git a/docs/bdr-pattern.md b/docs/bdr-pattern.md index a875960..9884de4 100644 --- a/docs/bdr-pattern.md +++ b/docs/bdr-pattern.md @@ -572,50 +572,43 @@ In the BDR Pattern, each excels in its own domain while building something great ### Q: How do I save modified objects back to the database? -**A: You don't.** Objects in the BDR Pattern are read-only and exist for querying data. When you need to modify data: +**A: You don't save the query object itself.** Objects in the BDR Pattern are read-only Query models: projections shaped for a screen, report, API response, or use case. When state must change, model that change as a Command. -1. **Make business decisions** in your application layer -2. **Issue a Command** - a clear, explicit write operation -3. **Execute simple write queries** - UPDATE, INSERT, DELETE statements +1. **Name the intent** - `ProcessOrder`, `DeactivateUser`, `ChangeShippingAddress` +2. **Let the Command side decide** - enforce invariants and make success/failure reasons explicit +3. **Persist the result** - execute the necessary UPDATE, INSERT, or DELETE in the command flow This follows the **CQRS (Command Query Responsibility Segregation)** principle: ```php -// Query side (BDR Pattern) -$order = $this->orderRepo->getOrder($id); -if ($order->canProcess()) { - // Command side (simple write) - $this->orderCommandRepo->markAsProcessed($id, new DateTime()); +// Query side (BDR Pattern): projection for the current use +$order = $this->orderQuery->getOrder($id); +if ($order->canShowProcessAction()) { + // The application may offer the action, but the Command owns the final decision. + $this->processOrder->execute($id, new DateTimeImmutable()); } -// orderCommandRepo might use simple SQL: +// processOrder may use explicit write SQL: // UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id ``` The separation is intentional: -- **Queries** can be complex, with JOINs and aggregations -- **Commands** should be simple and focused on changing state -- **Domain logic** lives in the query objects, not in the database writes +- **Queries** can use JOINs, aggregations, calculations, and projection SQL to shape data for a specific use +- **Query objects** are read-only structures for the current use and can be replaced when that use changes +- **Commands** carry business intent and invariants; writes are the persistence result of that decision ### Q: Is this the CQRS pattern? -**A: Yes, specifically the Query (read) side.** The BDR Pattern is a powerful implementation of CQRS's query side. +**A: Yes. BDR expresses the Query side of CQRS.** But this is not mainly about placing read repositories and write repositories in different locations. It is about separating concerns and models. -CQRS separates read and write responsibilities: -- **Query side (BDR Pattern)**: Complex reads with rich domain objects containing business logic -- **Command side**: Simple, focused writes that change state +CQRS is often introduced as a data-source or Repository placement pattern. That is an easy shape to draw, but it is not the essence. The essence is that Command and Query optimize for different concerns, so they should not be forced into one Repository or Entity model. -The BDR Pattern handles the complex part (queries) by combining: -- SQL's power for data retrieval -- Factories for transformation and enrichment -- Domain objects for business logic +- **Command side**: intent, behavior, invariants, and failure reasons. It answers, "May this action happen?" +- **Query side (BDR Pattern)**: projection, structure, and arrangement for the current reader. It answers, "What shape is useful to read now?" -Meanwhile, the command side remains straightforward: -- Direct UPDATE/INSERT/DELETE statements -- Event sourcing (if needed) -- Simple validation before writes +SQL already has this Query-side character. A `SELECT` can join, aggregate, calculate, and project a result into the exact structure needed without pretending that structure is the canonical domain model. In BDR, the SQL file defines that projection, and the factory/domain object gives it a typed PHP surface. -This separation makes both sides simpler and more maintainable. +The Query model may be disposable. If a screen, report, or API response changes, write another `SELECT` and another small read model. That is not a DRY violation; it is the point of CQRS: different concerns get different models. ### Q: Won't calling external APIs in factories slow down list retrievals? From a3b9979349409e2d21db0081f4b500bbe687ad48 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sat, 6 Jun 2026 02:17:16 +0900 Subject: [PATCH 2/4] Refine CQRS FAQ framing --- BDR_PATTERN-ja.md | 19 +++++++++++-------- BDR_PATTERN.md | 19 +++++++++++-------- docs/bdr-pattern-ja.md | 19 +++++++++++-------- docs/bdr-pattern.md | 19 +++++++++++-------- 4 files changed, 44 insertions(+), 32 deletions(-) diff --git a/BDR_PATTERN-ja.md b/BDR_PATTERN-ja.md index b246a0d..9c2922e 100644 --- a/BDR_PATTERN-ja.md +++ b/BDR_PATTERN-ja.md @@ -612,10 +612,10 @@ BDRパターンでは、それぞれが自身の領域で優秀さを発揮し ### Q: 変更されたオブジェクトをどうやってDBに書き戻す(保存する)のですか? -**A: BDR のクエリオブジェクト自体は保存しません。** BDR パターンのオブジェクトは読み取り専用の Query モデル、つまり画面、帳票、API レスポンス、ユースケースに合わせた projection です。状態を変更する必要がある場合は、その変更を Command として表します。 +**A: BDR のクエリオブジェクト自体は保存しません。** BDR パターンのオブジェクトは読み取り専用の Query モデル、つまり画面、帳票、API レスポンス、ユースケースに合わせた projection です。状態を変更する必要がある場合は、その変更を Command 側の意思決定として表します。 1. **意図に名前を付ける** - `ProcessOrder`、`DeactivateUser`、`ChangeShippingAddress` -2. **Command 側で判断する** - 不変条件を守り、成功/失敗の理由を明確にする +2. **Command モデルで判断する** - ドメインの整合性を守り、成功/失敗の理由を明確にする 3. **結果を永続化する** - 必要な UPDATE、INSERT、DELETE を Command の流れで実行する これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の原則に従います: @@ -633,18 +633,21 @@ if ($order->canShowProcessAction()) { ``` この分離は意図的です: -- **Query** は JOIN、集約、計算、projection SQL で用途に合わせて形を作れる -- **Query オブジェクト**は今読むための読み取り専用構造で、用途が変われば置き換えてよい -- **Command** は業務の意図と不変条件を表し、書き込みはその判断を永続化した結果である +- **Command モデル**はドメインの整合性を守り、業務上の行為を実行してよいかを判断する +- **Query モデル**は画面や帳票に合わせてデータを形作り、用途が変われば置き換えてよい +- **SQL** は projection に向いている。JOIN、集約、計算、非正規化された結果の形を自然に表せる ### Q: これはCQRSパターンですか? **A: はい。BDR は CQRS の Query 側を表現します。** ただし、それは Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。 -CQRS は、データソースや Repository の置き場所のパターンとして説明されることがあります。それは図にしやすい形ですが、本質ではありません。本質は、Command と Query が最適化する関心が違うため、それらを一つの Repository や Entity モデルに押し込まないことです。 +出発点は単純です。読み取りと書き込みでは、最適なモデルが違います。 -- **Command 側**:意図、振る舞い、不変条件、失敗理由を表す。「この行為は実行してよいか?」に答える -- **Query 側(BDR パターン)**:今の読み手に必要な projection、構造、並びを表す。「今読むにはどんな形が役に立つか?」に答える +書き込み側には、ドメインの整合性を守るモデルが必要です。そこでは意図、振る舞い、不変条件、失敗理由を扱います。「この業務上の行為は実行してよいか?」に答えるモデルです。 + +読み取り側では、画面、帳票、API レスポンスのために、非正規化されたフラットなデータが欲しいことがよくあります。「今表示するにはどんな形が役に立つか?」に答えるモデルです。同じ Repository や Entity モデルで両方を満たそうとするから無理が生じます。 + +CQRS はしばしば、データベースを分ける、インフラを分ける、Repository の置き場所を分ける、といった物理的なアーキテクチャとして誤解されます。それらは有用な実装上の選択肢になることはありますが、本質ではありません。本質は、Command は業務の意思決定であり、Query は表示のための構造である、という分離です。 SQL はそもそもこの Query 側の性質を持っています。`SELECT` は JOIN、集約、計算を使って、必要な形へ結果を projection できます。その構造を永続的な正規ドメインモデルであるかのように扱う必要はありません。BDR では、SQL ファイルがその projection を定義し、ファクトリやドメインオブジェクトが PHP の型として表面を与えます。 diff --git a/BDR_PATTERN.md b/BDR_PATTERN.md index aa893f3..bcaf2cc 100644 --- a/BDR_PATTERN.md +++ b/BDR_PATTERN.md @@ -564,10 +564,10 @@ In the BDR Pattern, each excels in its own domain while building something great ### Q: How do I save modified objects back to the database? -**A: You don't save the query object itself.** Objects in the BDR Pattern are read-only Query models: projections shaped for a screen, report, API response, or use case. When state must change, model that change as a Command. +**A: You don't save the query object itself.** Objects in the BDR Pattern are read-only Query models: projections shaped for a screen, report, API response, or use case. When state must change, model that change as a Command-side decision. 1. **Name the intent** - `ProcessOrder`, `DeactivateUser`, `ChangeShippingAddress` -2. **Let the Command side decide** - enforce invariants and make success/failure reasons explicit +2. **Let the Command model decide** - enforce domain consistency and make success/failure reasons explicit 3. **Persist the result** - execute the necessary UPDATE, INSERT, or DELETE in the command flow This follows the **CQRS (Command Query Responsibility Segregation)** principle: @@ -585,18 +585,21 @@ if ($order->canShowProcessAction()) { ``` The separation is intentional: -- **Queries** can use JOINs, aggregations, calculations, and projection SQL to shape data for a specific use -- **Query objects** are read-only structures for the current use and can be replaced when that use changes -- **Commands** carry business intent and invariants; writes are the persistence result of that decision +- **Command models** protect domain consistency and decide whether a business action may happen +- **Query models** shape data for display or reporting and can be replaced when that use changes +- **SQL** is naturally good at projection: JOINs, aggregations, calculations, and denormalized result shapes ### Q: Is this the CQRS pattern? **A: Yes. BDR expresses the Query side of CQRS.** But this is not mainly about placing read repositories and write repositories in different locations. It is about separating concerns and models. -CQRS is often introduced as a data-source or Repository placement pattern. That is an easy shape to draw, but it is not the essence. The essence is that Command and Query optimize for different concerns, so they should not be forced into one Repository or Entity model. +The starting point is simple: reads and writes want different models. -- **Command side**: intent, behavior, invariants, and failure reasons. It answers, "May this action happen?" -- **Query side (BDR Pattern)**: projection, structure, and arrangement for the current reader. It answers, "What shape is useful to read now?" +The write side needs a domain model that protects consistency. It carries intent, behavior, invariants, and failure reasons. It answers, "May this business action happen?" + +The read side often wants denormalized, flattened data for a screen, report, or API response. It answers, "What shape is useful to display now?" Trying to satisfy both with one Repository or Entity model creates friction. + +CQRS is often mistaken for a physical architecture: separate databases, separate infrastructure, separate repository locations. Those may be useful implementation choices, but they are not the essence. The essence is that Command is business decision, and Query is display structure. SQL already has this Query-side character. A `SELECT` can join, aggregate, calculate, and project a result into the exact structure needed without pretending that structure is the canonical domain model. In BDR, the SQL file defines that projection, and the factory/domain object gives it a typed PHP surface. diff --git a/docs/bdr-pattern-ja.md b/docs/bdr-pattern-ja.md index 4fd2ea9..b2519fa 100644 --- a/docs/bdr-pattern-ja.md +++ b/docs/bdr-pattern-ja.md @@ -622,10 +622,10 @@ BDRパターンでは、それぞれが自身の領域で優秀さを発揮し ### Q: 変更されたオブジェクトをどうやってDBに書き戻す(保存する)のですか? -**A: BDR のクエリオブジェクト自体は保存しません。** BDR パターンのオブジェクトは読み取り専用の Query モデル、つまり画面、帳票、API レスポンス、ユースケースに合わせた projection です。状態を変更する必要がある場合は、その変更を Command として表します。 +**A: BDR のクエリオブジェクト自体は保存しません。** BDR パターンのオブジェクトは読み取り専用の Query モデル、つまり画面、帳票、API レスポンス、ユースケースに合わせた projection です。状態を変更する必要がある場合は、その変更を Command 側の意思決定として表します。 1. **意図に名前を付ける** - `ProcessOrder`、`DeactivateUser`、`ChangeShippingAddress` -2. **Command 側で判断する** - 不変条件を守り、成功/失敗の理由を明確にする +2. **Command モデルで判断する** - ドメインの整合性を守り、成功/失敗の理由を明確にする 3. **結果を永続化する** - 必要な UPDATE、INSERT、DELETE を Command の流れで実行する これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の原則に従います: @@ -643,18 +643,21 @@ if ($order->canShowProcessAction()) { ``` この分離は意図的です: -- **Query** は JOIN、集約、計算、projection SQL で用途に合わせて形を作れる -- **Query オブジェクト**は今読むための読み取り専用構造で、用途が変われば置き換えてよい -- **Command** は業務の意図と不変条件を表し、書き込みはその判断を永続化した結果である +- **Command モデル**はドメインの整合性を守り、業務上の行為を実行してよいかを判断する +- **Query モデル**は画面や帳票に合わせてデータを形作り、用途が変われば置き換えてよい +- **SQL** は projection に向いている。JOIN、集約、計算、非正規化された結果の形を自然に表せる ### Q: これはCQRSパターンですか? **A: はい。BDR は CQRS の Query 側を表現します。** ただし、それは Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。 -CQRS は、データソースや Repository の置き場所のパターンとして説明されることがあります。それは図にしやすい形ですが、本質ではありません。本質は、Command と Query が最適化する関心が違うため、それらを一つの Repository や Entity モデルに押し込まないことです。 +出発点は単純です。読み取りと書き込みでは、最適なモデルが違います。 -- **Command 側**:意図、振る舞い、不変条件、失敗理由を表す。「この行為は実行してよいか?」に答える -- **Query 側(BDR パターン)**:今の読み手に必要な projection、構造、並びを表す。「今読むにはどんな形が役に立つか?」に答える +書き込み側には、ドメインの整合性を守るモデルが必要です。そこでは意図、振る舞い、不変条件、失敗理由を扱います。「この業務上の行為は実行してよいか?」に答えるモデルです。 + +読み取り側では、画面、帳票、API レスポンスのために、非正規化されたフラットなデータが欲しいことがよくあります。「今表示するにはどんな形が役に立つか?」に答えるモデルです。同じ Repository や Entity モデルで両方を満たそうとするから無理が生じます。 + +CQRS はしばしば、データベースを分ける、インフラを分ける、Repository の置き場所を分ける、といった物理的なアーキテクチャとして誤解されます。それらは有用な実装上の選択肢になることはありますが、本質ではありません。本質は、Command は業務の意思決定であり、Query は表示のための構造である、という分離です。 SQL はそもそもこの Query 側の性質を持っています。`SELECT` は JOIN、集約、計算を使って、必要な形へ結果を projection できます。その構造を永続的な正規ドメインモデルであるかのように扱う必要はありません。BDR では、SQL ファイルがその projection を定義し、ファクトリやドメインオブジェクトが PHP の型として表面を与えます。 diff --git a/docs/bdr-pattern.md b/docs/bdr-pattern.md index 9884de4..2a83e3e 100644 --- a/docs/bdr-pattern.md +++ b/docs/bdr-pattern.md @@ -572,10 +572,10 @@ In the BDR Pattern, each excels in its own domain while building something great ### Q: How do I save modified objects back to the database? -**A: You don't save the query object itself.** Objects in the BDR Pattern are read-only Query models: projections shaped for a screen, report, API response, or use case. When state must change, model that change as a Command. +**A: You don't save the query object itself.** Objects in the BDR Pattern are read-only Query models: projections shaped for a screen, report, API response, or use case. When state must change, model that change as a Command-side decision. 1. **Name the intent** - `ProcessOrder`, `DeactivateUser`, `ChangeShippingAddress` -2. **Let the Command side decide** - enforce invariants and make success/failure reasons explicit +2. **Let the Command model decide** - enforce domain consistency and make success/failure reasons explicit 3. **Persist the result** - execute the necessary UPDATE, INSERT, or DELETE in the command flow This follows the **CQRS (Command Query Responsibility Segregation)** principle: @@ -593,18 +593,21 @@ if ($order->canShowProcessAction()) { ``` The separation is intentional: -- **Queries** can use JOINs, aggregations, calculations, and projection SQL to shape data for a specific use -- **Query objects** are read-only structures for the current use and can be replaced when that use changes -- **Commands** carry business intent and invariants; writes are the persistence result of that decision +- **Command models** protect domain consistency and decide whether a business action may happen +- **Query models** shape data for display or reporting and can be replaced when that use changes +- **SQL** is naturally good at projection: JOINs, aggregations, calculations, and denormalized result shapes ### Q: Is this the CQRS pattern? **A: Yes. BDR expresses the Query side of CQRS.** But this is not mainly about placing read repositories and write repositories in different locations. It is about separating concerns and models. -CQRS is often introduced as a data-source or Repository placement pattern. That is an easy shape to draw, but it is not the essence. The essence is that Command and Query optimize for different concerns, so they should not be forced into one Repository or Entity model. +The starting point is simple: reads and writes want different models. -- **Command side**: intent, behavior, invariants, and failure reasons. It answers, "May this action happen?" -- **Query side (BDR Pattern)**: projection, structure, and arrangement for the current reader. It answers, "What shape is useful to read now?" +The write side needs a domain model that protects consistency. It carries intent, behavior, invariants, and failure reasons. It answers, "May this business action happen?" + +The read side often wants denormalized, flattened data for a screen, report, or API response. It answers, "What shape is useful to display now?" Trying to satisfy both with one Repository or Entity model creates friction. + +CQRS is often mistaken for a physical architecture: separate databases, separate infrastructure, separate repository locations. Those may be useful implementation choices, but they are not the essence. The essence is that Command is business decision, and Query is display structure. SQL already has this Query-side character. A `SELECT` can join, aggregate, calculate, and project a result into the exact structure needed without pretending that structure is the canonical domain model. In BDR, the SQL file defines that projection, and the factory/domain object gives it a typed PHP surface. From 69748462408f64f2a5d0eb91b743b62b921e76a2 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sat, 6 Jun 2026 02:26:26 +0900 Subject: [PATCH 3/4] Align BDR CQRS FAQ with rich read models --- BDR_PATTERN-ja.md | 42 ++++++++++++++++++++++-------------------- BDR_PATTERN.md | 36 +++++++++++++++++++----------------- docs/bdr-pattern-ja.md | 42 ++++++++++++++++++++++-------------------- docs/bdr-pattern.md | 36 +++++++++++++++++++----------------- 4 files changed, 82 insertions(+), 74 deletions(-) diff --git a/BDR_PATTERN-ja.md b/BDR_PATTERN-ja.md index 9c2922e..1b5df56 100644 --- a/BDR_PATTERN-ja.md +++ b/BDR_PATTERN-ja.md @@ -114,7 +114,7 @@ BDRパターンは重要なことを達成します:**SQL基盤による真の - **明確な意図** - クエリ(データ読み取り)とコマンド(データ変更)の分離 ```php -// ドメインオブジェクトでのDIの力を活用 +// 読み取り側の問いにDIの力を活用 final readonly class UserDomainObject { public function __construct( @@ -125,7 +125,7 @@ final readonly class UserDomainObject private PermissionService $permissionService, ) {} - // 注入されたサービスによる動的なビジネスルール + // 注入されたサービスによる読み取り側の問い public function canEdit(Document $document): bool { // ORMエンティティでは不可能 - 外部サービスに依存 @@ -139,7 +139,7 @@ final readonly class UserDomainObject } ``` -BDRパターンでは、オブジェクトは単なるデータコンテナではなく、ビジネスロジックを含むドメインオブジェクトです。これらはビジネスドメインに関する質問に答えますが、データベース自体を変更することはありません。 +BDRパターンでは、オブジェクトは単なるデータコンテナではなく、振る舞いを持つ読み取り側のドメインオブジェクトです。現在の projection やユーザー体験に関する問いに答えますが、状態変更前の最終判断は Command モデルが行います。 ## 実装ガイド @@ -250,13 +250,13 @@ final readonly class OrderDomainObject public float $tax, // 地域別計算済み税額 public float $shipping, // 計算済み送料 public float $total, // 総合計 - public bool $canFulfill, // 適用済みビジネスルール + public bool $canFulfill, // 読み取り側ルールの結果 public array $insufficientStockItems, // 在庫不足商品リスト - // 注入されたビジネスルールエンジン - ORMでは不可能 + // 注入された読み取り側ルールエンジン - ORMでは不可能 private BusinessRuleEngine $ruleEngine, ) {} - // ドメインオブジェクトの振る舞い + // 読み取り側ドメインオブジェクトの振る舞い public function getDisplayTotal(): string { return '$' . number_format($this->total, 2); @@ -277,12 +277,12 @@ final readonly class OrderDomainObject return $this->status === 'pending'; } - public function canProcess(): bool + public function canShowProcessAction(): bool { return $this->canFulfill && $this->isPending(); } - // 注入されたサービスによる動的なビジネスルール + // 注入されたサービスによる読み取り側の優先度分類 public function getBusinessPriority(): string { // ORMエンティティでは不可能 - 外部サービスに依存 @@ -315,7 +315,7 @@ BDRパターンの重要な利点の一つは、**テストがシンプルで信 1. **SQLクエリ**:入力に対して正しいデータを返すか? 2. **ファクトリー**:データを正しくドメインオブジェクトに変換するか? -3. **ドメインオブジェクト**:ビジネスルールを正しく実装しているか? +3. **ドメインオブジェクト**:読み取り側の振る舞いを正しく実装しているか? これらが個別に正しければ、組み合わせは必然的に正しくなります。**論理的構造です。** @@ -400,7 +400,7 @@ class OrderDomainObjectTest extends TestCase // 振る舞いをテスト $this->assertEquals('$2,660.00', $order->getDisplayTotal()); $this->assertEquals(8.0, $order->getTaxRate()); - $this->assertTrue($order->canProcess()); + $this->assertTrue($order->canShowProcessAction()); $this->assertFalse($order->hasInsufficientStock()); } } @@ -612,17 +612,15 @@ BDRパターンでは、それぞれが自身の領域で優秀さを発揮し ### Q: 変更されたオブジェクトをどうやってDBに書き戻す(保存する)のですか? -**A: BDR のクエリオブジェクト自体は保存しません。** BDR パターンのオブジェクトは読み取り専用の Query モデル、つまり画面、帳票、API レスポンス、ユースケースに合わせた projection です。状態を変更する必要がある場合は、その変更を Command 側の意思決定として表します。 +**A: BDR の読み取りオブジェクトを保存するのではなく、明示的な書き込み経路を使います。** BDRパターンのオブジェクトは、画面、帳票、API レスポンス、ユースケースに合わせた読み取り側の projection です。オブジェクト自身は保存しません。 -1. **意図に名前を付ける** - `ProcessOrder`、`DeactivateUser`、`ChangeShippingAddress` -2. **Command モデルで判断する** - ドメインの整合性を守り、成功/失敗の理由を明確にする -3. **結果を永続化する** - 必要な UPDATE、INSERT、DELETE を Command の流れで実行する - -これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の原則に従います: +1. 現在の状態や表示可能な操作を示すために、必要なら BDR の Query モデルを読む +2. 状態変更には Command またはアプリケーションの書き込みユースケースを呼ぶ +3. その書き込み経路で書き込み側の不変条件を検証し、UPDATE、INSERT、DELETE、または別の書き込み手段で結果を永続化する ```php // Query側(BDRパターン): 今の用途に必要なprojection -$order = $this->orderQuery->getOrder($id); +$order = $this->orderRepo->getOrder($id); if ($order->canShowProcessAction()) { // アプリケーションは操作を提示できる。ただし最終判断はCommand側が行う。 $this->processOrder->execute($id, new DateTimeImmutable()); @@ -632,14 +630,16 @@ if ($order->canShowProcessAction()) { // UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id ``` -この分離は意図的です: +`canShowProcessAction()` は表示のための読み取り側の派生判断です。`ProcessOrder` は書き込み側の不変条件をあらためて検証しなければなりません。BDR は Command モデルを定義しません。Ray.MediaQuery は DML を実行できますが、それを書き込み手段として選ぶかどうかはアプリケーション側の設計です。 + +これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の区別に従います: +- **Query モデル**は画面や帳票に合わせてデータを形作り、派生・表示の振る舞いを持つことができる - **Command モデル**はドメインの整合性を守り、業務上の行為を実行してよいかを判断する -- **Query モデル**は画面や帳票に合わせてデータを形作り、用途が変われば置き換えてよい - **SQL** は projection に向いている。JOIN、集約、計算、非正規化された結果の形を自然に表せる ### Q: これはCQRSパターンですか? -**A: はい。BDR は CQRS の Query 側を表現します。** ただし、それは Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。 +**A: BDRパターンは CQRS の Query 側に適用できます。ただし、より正確には rich read model のパターンです。** Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。 出発点は単純です。読み取りと書き込みでは、最適なモデルが違います。 @@ -649,6 +649,8 @@ if ($order->canShowProcessAction()) { CQRS はしばしば、データベースを分ける、インフラを分ける、Repository の置き場所を分ける、といった物理的なアーキテクチャとして誤解されます。それらは有用な実装上の選択肢になることはありますが、本質ではありません。本質は、Command は業務の意思決定であり、Query は表示のための構造である、という分離です。 +BDR は薄い DTO に限定されません。読み取りモデルは、派生・表示のロジックである限り、振る舞いを持てます。合計、ラベル、表示可否、読み取り側の優先度分類など、現在の projection に関する問いに答える振る舞いです。状態変更の不変条件は Command 側に置きます。 + SQL はそもそもこの Query 側の性質を持っています。`SELECT` は JOIN、集約、計算を使って、必要な形へ結果を projection できます。その構造を永続的な正規ドメインモデルであるかのように扱う必要はありません。BDR では、SQL ファイルがその projection を定義し、ファクトリやドメインオブジェクトが PHP の型として表面を与えます。 Query モデルは使い捨てであってもかまいません。画面、帳票、API レスポンスが変われば、別の `SELECT` と小さな読み取りモデルを作ればよい。それは DRY 違反ではなく、CQRS の要点です。関心が違うものには、違うモデルを与えます。 diff --git a/BDR_PATTERN.md b/BDR_PATTERN.md index bcaf2cc..9b4d921 100644 --- a/BDR_PATTERN.md +++ b/BDR_PATTERN.md @@ -127,7 +127,7 @@ final readonly class UserDomainObject private PermissionService $permissionService, ) {} - // Dynamic business rules through injected service + // Read-side business questions through injected service public function canEdit(Document $document): bool { // Impossible with ORM entities - depends on external service @@ -141,7 +141,7 @@ final readonly class UserDomainObject } ``` -In the BDR Pattern, objects are not mere data containers but domain objects containing business logic. They answer questions about the business domain but don't change the database themselves. +In the BDR Pattern, objects are not mere data containers but read-side domain objects with behavior. They answer questions about the current projection and user experience, but Command models still make the final decision before state changes. ## Implementation Guide @@ -252,13 +252,13 @@ final readonly class OrderDomainObject public float $tax, // Calculated by region public float $shipping, // Calculated shipping public float $total, // Complete total - public bool $canFulfill, // Business rule applied + public bool $canFulfill, // Read-side rule result public array $insufficientStockItems, // List of insufficient stock items // Injected business rule engine - impossible with ORM private BusinessRuleEngine $ruleEngine, ) {} - // Domain object behavior + // Read-side domain object behavior public function getDisplayTotal(): string { return '$' . number_format($this->total, 2); @@ -279,12 +279,12 @@ final readonly class OrderDomainObject return $this->status === 'pending'; } - public function canProcess(): bool + public function canShowProcessAction(): bool { return $this->canFulfill && $this->isPending(); } - // Dynamic business rules through injected service + // Read-side priority through injected service public function getBusinessPriority(): string { // Impossible with ORM entities - depends on external service @@ -317,7 +317,7 @@ Because each layer is **independent**, if each is tested individually, the combi 1. **SQL Query**: Does it return correct data for the input? 2. **Factory**: Does it correctly transform data into domain objects? -3. **Domain Object**: Does it correctly implement business rules? +3. **Domain Object**: Does it correctly implement read-side behavior? If these are individually correct, the combination is necessarily correct. **It's a logical structure.** @@ -564,17 +564,15 @@ In the BDR Pattern, each excels in its own domain while building something great ### Q: How do I save modified objects back to the database? -**A: You don't save the query object itself.** Objects in the BDR Pattern are read-only Query models: projections shaped for a screen, report, API response, or use case. When state must change, model that change as a Command-side decision. +**A: Use an explicit write path, not the BDR read object.** A BDR object is a read-side projection shaped for a screen, report, API response, or use case. It does not save itself. -1. **Name the intent** - `ProcessOrder`, `DeactivateUser`, `ChangeShippingAddress` -2. **Let the Command model decide** - enforce domain consistency and make success/failure reasons explicit -3. **Persist the result** - execute the necessary UPDATE, INSERT, or DELETE in the command flow - -This follows the **CQRS (Command Query Responsibility Segregation)** principle: +1. Read a BDR Query model when it helps present the current state or available actions. +2. Call a Command or application write use case for the state change. +3. In that write path, validate write-side invariants and persist the result with UPDATE, INSERT, DELETE, or another write mechanism. ```php // Query side (BDR Pattern): projection for the current use -$order = $this->orderQuery->getOrder($id); +$order = $this->orderRepo->getOrder($id); if ($order->canShowProcessAction()) { // The application may offer the action, but the Command owns the final decision. $this->processOrder->execute($id, new DateTimeImmutable()); @@ -584,14 +582,16 @@ if ($order->canShowProcessAction()) { // UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id ``` -The separation is intentional: +`canShowProcessAction()` is read-side derivation for presentation. `ProcessOrder` must still enforce the write-side invariant. BDR does not define the Command model; Ray.MediaQuery can execute DML if that is the write mechanism you choose. + +This follows the **CQRS (Command Query Responsibility Segregation)** distinction: +- **Query models** shape data for display or reporting and may contain derivation/presentation behavior - **Command models** protect domain consistency and decide whether a business action may happen -- **Query models** shape data for display or reporting and can be replaced when that use changes - **SQL** is naturally good at projection: JOINs, aggregations, calculations, and denormalized result shapes ### Q: Is this the CQRS pattern? -**A: Yes. BDR expresses the Query side of CQRS.** But this is not mainly about placing read repositories and write repositories in different locations. It is about separating concerns and models. +**A: BDR fits the Query side of CQRS, but more precisely it is a rich read-model pattern.** It is not mainly about placing read repositories and write repositories in different locations. It is about separating concerns and models. The starting point is simple: reads and writes want different models. @@ -601,6 +601,8 @@ The read side often wants denormalized, flattened data for a screen, report, or CQRS is often mistaken for a physical architecture: separate databases, separate infrastructure, separate repository locations. Those may be useful implementation choices, but they are not the essence. The essence is that Command is business decision, and Query is display structure. +BDR is not limited to a thin DTO. Its read model can expose behavior, as long as that behavior is derivation or presentation logic: totals, labels, visibility, read-side priority, or other answers about the current projection. State-changing invariants stay on the Command side. + SQL already has this Query-side character. A `SELECT` can join, aggregate, calculate, and project a result into the exact structure needed without pretending that structure is the canonical domain model. In BDR, the SQL file defines that projection, and the factory/domain object gives it a typed PHP surface. The Query model may be disposable. If a screen, report, or API response changes, write another `SELECT` and another small read model. That is not a DRY violation; it is the point of CQRS: different concerns get different models. diff --git a/docs/bdr-pattern-ja.md b/docs/bdr-pattern-ja.md index b2519fa..d7213c1 100644 --- a/docs/bdr-pattern-ja.md +++ b/docs/bdr-pattern-ja.md @@ -124,7 +124,7 @@ BDRパターンは重要なことを達成します:**SQL基盤による真の - **明確な意図** - クエリ(データ読み取り)とコマンド(データ変更)の分離 ```php -// ドメインオブジェクトでのDIの力を活用 +// 読み取り側の問いにDIの力を活用 final readonly class UserDomainObject { public function __construct( @@ -135,7 +135,7 @@ final readonly class UserDomainObject private PermissionService $permissionService, ) {} - // 注入されたサービスによる動的なビジネスルール + // 注入されたサービスによる読み取り側の問い public function canEdit(Document $document): bool { // ORMエンティティでは不可能 - 外部サービスに依存 @@ -149,7 +149,7 @@ final readonly class UserDomainObject } ``` -BDRパターンでは、オブジェクトは単なるデータコンテナではなく、ビジネスロジックを含むドメインオブジェクトです。これらはビジネスドメインに関する質問に答えますが、データベース自体を変更することはありません。 +BDRパターンでは、オブジェクトは単なるデータコンテナではなく、振る舞いを持つ読み取り側のドメインオブジェクトです。現在の projection やユーザー体験に関する問いに答えますが、状態変更前の最終判断は Command モデルが行います。 ## 実装ガイド @@ -260,13 +260,13 @@ final readonly class OrderDomainObject public float $tax, // 地域別計算済み税額 public float $shipping, // 計算済み送料 public float $total, // 総合計 - public bool $canFulfill, // 適用済みビジネスルール + public bool $canFulfill, // 読み取り側ルールの結果 public array $insufficientStockItems, // 在庫不足商品リスト - // 注入されたビジネスルールエンジン - ORMでは不可能 + // 注入された読み取り側ルールエンジン - ORMでは不可能 private BusinessRuleEngine $ruleEngine, ) {} - // ドメインオブジェクトの振る舞い + // 読み取り側ドメインオブジェクトの振る舞い public function getDisplayTotal(): string { return '$' . number_format($this->total, 2); @@ -287,12 +287,12 @@ final readonly class OrderDomainObject return $this->status === 'pending'; } - public function canProcess(): bool + public function canShowProcessAction(): bool { return $this->canFulfill && $this->isPending(); } - // 注入されたサービスによる動的なビジネスルール + // 注入されたサービスによる読み取り側の優先度分類 public function getBusinessPriority(): string { // ORMエンティティでは不可能 - 外部サービスに依存 @@ -325,7 +325,7 @@ BDRパターンの重要な利点の一つは、**テストがシンプルで信 1. **SQLクエリ**:入力に対して正しいデータを返すか? 2. **ファクトリー**:データを正しくドメインオブジェクトに変換するか? -3. **ドメインオブジェクト**:ビジネスルールを正しく実装しているか? +3. **ドメインオブジェクト**:読み取り側の振る舞いを正しく実装しているか? これらが個別に正しければ、組み合わせは必然的に正しくなります。**論理的構造です。** @@ -410,7 +410,7 @@ class OrderDomainObjectTest extends TestCase // 振る舞いをテスト $this->assertEquals('$2,660.00', $order->getDisplayTotal()); $this->assertEquals(8.0, $order->getTaxRate()); - $this->assertTrue($order->canProcess()); + $this->assertTrue($order->canShowProcessAction()); $this->assertFalse($order->hasInsufficientStock()); } } @@ -622,17 +622,15 @@ BDRパターンでは、それぞれが自身の領域で優秀さを発揮し ### Q: 変更されたオブジェクトをどうやってDBに書き戻す(保存する)のですか? -**A: BDR のクエリオブジェクト自体は保存しません。** BDR パターンのオブジェクトは読み取り専用の Query モデル、つまり画面、帳票、API レスポンス、ユースケースに合わせた projection です。状態を変更する必要がある場合は、その変更を Command 側の意思決定として表します。 +**A: BDR の読み取りオブジェクトを保存するのではなく、明示的な書き込み経路を使います。** BDRパターンのオブジェクトは、画面、帳票、API レスポンス、ユースケースに合わせた読み取り側の projection です。オブジェクト自身は保存しません。 -1. **意図に名前を付ける** - `ProcessOrder`、`DeactivateUser`、`ChangeShippingAddress` -2. **Command モデルで判断する** - ドメインの整合性を守り、成功/失敗の理由を明確にする -3. **結果を永続化する** - 必要な UPDATE、INSERT、DELETE を Command の流れで実行する - -これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の原則に従います: +1. 現在の状態や表示可能な操作を示すために、必要なら BDR の Query モデルを読む +2. 状態変更には Command またはアプリケーションの書き込みユースケースを呼ぶ +3. その書き込み経路で書き込み側の不変条件を検証し、UPDATE、INSERT、DELETE、または別の書き込み手段で結果を永続化する ```php // Query側(BDRパターン): 今の用途に必要なprojection -$order = $this->orderQuery->getOrder($id); +$order = $this->orderRepo->getOrder($id); if ($order->canShowProcessAction()) { // アプリケーションは操作を提示できる。ただし最終判断はCommand側が行う。 $this->processOrder->execute($id, new DateTimeImmutable()); @@ -642,14 +640,16 @@ if ($order->canShowProcessAction()) { // UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id ``` -この分離は意図的です: +`canShowProcessAction()` は表示のための読み取り側の派生判断です。`ProcessOrder` は書き込み側の不変条件をあらためて検証しなければなりません。BDR は Command モデルを定義しません。Ray.MediaQuery は DML を実行できますが、それを書き込み手段として選ぶかどうかはアプリケーション側の設計です。 + +これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の区別に従います: +- **Query モデル**は画面や帳票に合わせてデータを形作り、派生・表示の振る舞いを持つことができる - **Command モデル**はドメインの整合性を守り、業務上の行為を実行してよいかを判断する -- **Query モデル**は画面や帳票に合わせてデータを形作り、用途が変われば置き換えてよい - **SQL** は projection に向いている。JOIN、集約、計算、非正規化された結果の形を自然に表せる ### Q: これはCQRSパターンですか? -**A: はい。BDR は CQRS の Query 側を表現します。** ただし、それは Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。 +**A: BDRパターンは CQRS の Query 側に適用できます。ただし、より正確には rich read model のパターンです。** Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。 出発点は単純です。読み取りと書き込みでは、最適なモデルが違います。 @@ -659,6 +659,8 @@ if ($order->canShowProcessAction()) { CQRS はしばしば、データベースを分ける、インフラを分ける、Repository の置き場所を分ける、といった物理的なアーキテクチャとして誤解されます。それらは有用な実装上の選択肢になることはありますが、本質ではありません。本質は、Command は業務の意思決定であり、Query は表示のための構造である、という分離です。 +BDR は薄い DTO に限定されません。読み取りモデルは、派生・表示のロジックである限り、振る舞いを持てます。合計、ラベル、表示可否、読み取り側の優先度分類など、現在の projection に関する問いに答える振る舞いです。状態変更の不変条件は Command 側に置きます。 + SQL はそもそもこの Query 側の性質を持っています。`SELECT` は JOIN、集約、計算を使って、必要な形へ結果を projection できます。その構造を永続的な正規ドメインモデルであるかのように扱う必要はありません。BDR では、SQL ファイルがその projection を定義し、ファクトリやドメインオブジェクトが PHP の型として表面を与えます。 Query モデルは使い捨てであってもかまいません。画面、帳票、API レスポンスが変われば、別の `SELECT` と小さな読み取りモデルを作ればよい。それは DRY 違反ではなく、CQRS の要点です。関心が違うものには、違うモデルを与えます。 diff --git a/docs/bdr-pattern.md b/docs/bdr-pattern.md index 2a83e3e..624cca4 100644 --- a/docs/bdr-pattern.md +++ b/docs/bdr-pattern.md @@ -135,7 +135,7 @@ final readonly class UserDomainObject private PermissionService $permissionService, ) {} - // Dynamic business rules through injected service + // Read-side business questions through injected service public function canEdit(Document $document): bool { // Impossible with ORM entities - depends on external service @@ -149,7 +149,7 @@ final readonly class UserDomainObject } ``` -In the BDR Pattern, objects are not mere data containers but domain objects containing business logic. They answer questions about the business domain but don't change the database themselves. +In the BDR Pattern, objects are not mere data containers but read-side domain objects with behavior. They answer questions about the current projection and user experience, but Command models still make the final decision before state changes. ## Implementation Guide @@ -260,13 +260,13 @@ final readonly class OrderDomainObject public float $tax, // Calculated by region public float $shipping, // Calculated shipping public float $total, // Complete total - public bool $canFulfill, // Business rule applied + public bool $canFulfill, // Read-side rule result public array $insufficientStockItems, // List of insufficient stock items // Injected business rule engine - impossible with ORM private BusinessRuleEngine $ruleEngine, ) {} - // Domain object behavior + // Read-side domain object behavior public function getDisplayTotal(): string { return '$' . number_format($this->total, 2); @@ -287,12 +287,12 @@ final readonly class OrderDomainObject return $this->status === 'pending'; } - public function canProcess(): bool + public function canShowProcessAction(): bool { return $this->canFulfill && $this->isPending(); } - // Dynamic business rules through injected service + // Read-side priority through injected service public function getBusinessPriority(): string { // Impossible with ORM entities - depends on external service @@ -325,7 +325,7 @@ Because each layer is **independent**, if each is tested individually, the combi 1. **SQL Query**: Does it return correct data for the input? 2. **Factory**: Does it correctly transform data into domain objects? -3. **Domain Object**: Does it correctly implement business rules? +3. **Domain Object**: Does it correctly implement read-side behavior? If these are individually correct, the combination is necessarily correct. **It's a logical structure.** @@ -572,17 +572,15 @@ In the BDR Pattern, each excels in its own domain while building something great ### Q: How do I save modified objects back to the database? -**A: You don't save the query object itself.** Objects in the BDR Pattern are read-only Query models: projections shaped for a screen, report, API response, or use case. When state must change, model that change as a Command-side decision. +**A: Use an explicit write path, not the BDR read object.** A BDR object is a read-side projection shaped for a screen, report, API response, or use case. It does not save itself. -1. **Name the intent** - `ProcessOrder`, `DeactivateUser`, `ChangeShippingAddress` -2. **Let the Command model decide** - enforce domain consistency and make success/failure reasons explicit -3. **Persist the result** - execute the necessary UPDATE, INSERT, or DELETE in the command flow - -This follows the **CQRS (Command Query Responsibility Segregation)** principle: +1. Read a BDR Query model when it helps present the current state or available actions. +2. Call a Command or application write use case for the state change. +3. In that write path, validate write-side invariants and persist the result with UPDATE, INSERT, DELETE, or another write mechanism. ```php // Query side (BDR Pattern): projection for the current use -$order = $this->orderQuery->getOrder($id); +$order = $this->orderRepo->getOrder($id); if ($order->canShowProcessAction()) { // The application may offer the action, but the Command owns the final decision. $this->processOrder->execute($id, new DateTimeImmutable()); @@ -592,14 +590,16 @@ if ($order->canShowProcessAction()) { // UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id ``` -The separation is intentional: +`canShowProcessAction()` is read-side derivation for presentation. `ProcessOrder` must still enforce the write-side invariant. BDR does not define the Command model; Ray.MediaQuery can execute DML if that is the write mechanism you choose. + +This follows the **CQRS (Command Query Responsibility Segregation)** distinction: +- **Query models** shape data for display or reporting and may contain derivation/presentation behavior - **Command models** protect domain consistency and decide whether a business action may happen -- **Query models** shape data for display or reporting and can be replaced when that use changes - **SQL** is naturally good at projection: JOINs, aggregations, calculations, and denormalized result shapes ### Q: Is this the CQRS pattern? -**A: Yes. BDR expresses the Query side of CQRS.** But this is not mainly about placing read repositories and write repositories in different locations. It is about separating concerns and models. +**A: BDR fits the Query side of CQRS, but more precisely it is a rich read-model pattern.** It is not mainly about placing read repositories and write repositories in different locations. It is about separating concerns and models. The starting point is simple: reads and writes want different models. @@ -609,6 +609,8 @@ The read side often wants denormalized, flattened data for a screen, report, or CQRS is often mistaken for a physical architecture: separate databases, separate infrastructure, separate repository locations. Those may be useful implementation choices, but they are not the essence. The essence is that Command is business decision, and Query is display structure. +BDR is not limited to a thin DTO. Its read model can expose behavior, as long as that behavior is derivation or presentation logic: totals, labels, visibility, read-side priority, or other answers about the current projection. State-changing invariants stay on the Command side. + SQL already has this Query-side character. A `SELECT` can join, aggregate, calculate, and project a result into the exact structure needed without pretending that structure is the canonical domain model. In BDR, the SQL file defines that projection, and the factory/domain object gives it a typed PHP surface. The Query model may be disposable. If a screen, report, or API response changes, write another `SELECT` and another small read model. That is not a DRY violation; it is the point of CQRS: different concerns get different models. From 7761824a938c70e4aac084a617213b0656b3aae9 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sat, 6 Jun 2026 03:09:07 +0900 Subject: [PATCH 4/4] Soften ORM comparison in BDR docs --- BDR_PATTERN-ja.md | 2 +- BDR_PATTERN.md | 4 ++-- docs/bdr-pattern-ja.md | 2 +- docs/bdr-pattern.md | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/BDR_PATTERN-ja.md b/BDR_PATTERN-ja.md index 1b5df56..91b361d 100644 --- a/BDR_PATTERN-ja.md +++ b/BDR_PATTERN-ja.md @@ -252,7 +252,7 @@ final readonly class OrderDomainObject public float $total, // 総合計 public bool $canFulfill, // 読み取り側ルールの結果 public array $insufficientStockItems, // 在庫不足商品リスト - // 注入された読み取り側ルールエンジン - ORMでは不可能 + // 注入された読み取り側ルールエンジン - ORMでは困難 private BusinessRuleEngine $ruleEngine, ) {} diff --git a/BDR_PATTERN.md b/BDR_PATTERN.md index 9b4d921..772d1be 100644 --- a/BDR_PATTERN.md +++ b/BDR_PATTERN.md @@ -130,7 +130,7 @@ final readonly class UserDomainObject // Read-side business questions through injected service public function canEdit(Document $document): bool { - // Impossible with ORM entities - depends on external service + // Difficult with ORM entities - depends on external service // Test env: FakePermissionService (everyone can edit) // Production: RealPermissionService (complex permission checks) return $this->permissionService->canEdit($this, $document); @@ -287,7 +287,7 @@ final readonly class OrderDomainObject // Read-side priority through injected service public function getBusinessPriority(): string { - // Impossible with ORM entities - depends on external service + // Difficult with ORM entities - depends on external service // Test environment: Relaxed thresholds (e.g., high priority at $100+) // Production: Strict thresholds (e.g., high priority at $10,000+) // Peak season: Different thresholds diff --git a/docs/bdr-pattern-ja.md b/docs/bdr-pattern-ja.md index d7213c1..dcdf020 100644 --- a/docs/bdr-pattern-ja.md +++ b/docs/bdr-pattern-ja.md @@ -262,7 +262,7 @@ final readonly class OrderDomainObject public float $total, // 総合計 public bool $canFulfill, // 読み取り側ルールの結果 public array $insufficientStockItems, // 在庫不足商品リスト - // 注入された読み取り側ルールエンジン - ORMでは不可能 + // 注入された読み取り側ルールエンジン - ORMでは困難 private BusinessRuleEngine $ruleEngine, ) {} diff --git a/docs/bdr-pattern.md b/docs/bdr-pattern.md index 624cca4..95e18bd 100644 --- a/docs/bdr-pattern.md +++ b/docs/bdr-pattern.md @@ -138,7 +138,7 @@ final readonly class UserDomainObject // Read-side business questions through injected service public function canEdit(Document $document): bool { - // Impossible with ORM entities - depends on external service + // Difficult with ORM entities - depends on external service // Test env: FakePermissionService (everyone can edit) // Production: RealPermissionService (complex permission checks) return $this->permissionService->canEdit($this, $document); @@ -295,7 +295,7 @@ final readonly class OrderDomainObject // Read-side priority through injected service public function getBusinessPriority(): string { - // Impossible with ORM entities - depends on external service + // Difficult with ORM entities - depends on external service // Test environment: Relaxed thresholds (e.g., high priority at $100+) // Production: Strict thresholds (e.g., high priority at $10,000+) // Peak season: Different thresholds