Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 48 additions & 43 deletions BDR_PATTERN-ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ BDRパターンは重要なことを達成します:**SQL基盤による真の
- **明確な意図** - クエリ(データ読み取り)とコマンド(データ変更)の分離

```php
// ドメインオブジェクトでのDIの力を活用
// 読み取り側の問いにDIの力を活用
final readonly class UserDomainObject
{
public function __construct(
Expand All @@ -125,7 +125,7 @@ final readonly class UserDomainObject
private PermissionService $permissionService,
) {}

// 注入されたサービスによる動的なビジネスルール
// 注入されたサービスによる読み取り側の問い
public function canEdit(Document $document): bool
{
// ORMエンティティでは不可能 - 外部サービスに依存
Expand All @@ -139,7 +139,7 @@ final readonly class UserDomainObject
}
```

BDRパターンでは、オブジェクトは単なるデータコンテナではなく、ビジネスロジックを含むドメインオブジェクトです。これらはビジネスドメインに関する質問に答えますが、データベース自体を変更することはありません
BDRパターンでは、オブジェクトは単なるデータコンテナではなく、振る舞いを持つ読み取り側のドメインオブジェクトです。現在の projection やユーザー体験に関する問いに答えますが、状態変更前の最終判断は Command モデルが行います

## 実装ガイド

Expand Down Expand Up @@ -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);
Expand All @@ -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エンティティでは不可能 - 外部サービスに依存
Expand Down Expand Up @@ -315,7 +315,7 @@ BDRパターンの重要な利点の一つは、**テストがシンプルで信

1. **SQLクエリ**:入力に対して正しいデータを返すか?
2. **ファクトリー**:データを正しくドメインオブジェクトに変換するか?
3. **ドメインオブジェクト**:ビジネスルールを正しく実装しているか
3. **ドメインオブジェクト**:読み取り側の振る舞いを正しく実装しているか

これらが個別に正しければ、組み合わせは必然的に正しくなります。**論理的構造です。**

Expand Down Expand Up @@ -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());
}
}
Expand Down Expand Up @@ -608,54 +608,52 @@ SQL(宣言的、集合ベース)とOOP(命令的、オブジェクトベ

BDRパターンでは、それぞれが自身の領域で優秀さを発揮しながら、共により大きなものを構築します。

## FAQ & アーキテクチャのヒント
## FAQ

### Q: 変更されたオブジェクトをどうやってDBに書き戻す(保存する)のですか?

**A: 書き戻しません。** BDRパターンのオブジェクトは読み取り専用であり、データのクエリのために存在します。データを変更する必要がある場合:
**A: BDR の読み取りオブジェクトを保存するのではなく、明示的な書き込み経路を使います。** BDRパターンのオブジェクトは、画面、帳票、API レスポンス、ユースケースに合わせた読み取り側の projection です。オブジェクト自身は保存しません。

1. **アプリケーション層でビジネス判断を行う**
2. **コマンドを発行する** - 明確で明示的な書き込み操作
3. **シンプルな書き込みクエリを実行** - UPDATE、INSERT、DELETE文

これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の原則に従います:
1. 現在の状態や表示可能な操作を示すために、必要なら BDR の Query モデルを読む
2. 状態変更には Command またはアプリケーションの書き込みユースケースを呼ぶ
3. その書き込み経路で書き込み側の不変条件を検証し、UPDATE、INSERT、DELETE、または別の書き込み手段で結果を永続化する

```php
// クエリ側(BDRパターン)
// Query側(BDRパターン): 今の用途に必要なprojection
$order = $this->orderRepo->getOrder($id);
if ($order->canProcess()) {
// コマンド側(シンプルな書き込み)
$this->orderCommandRepo->markAsProcessed($id, new DateTime());
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や集約を含むことができる
- **コマンド**はシンプルで、状態変更に集中すべき
- **ドメインロジック**はクエリオブジェクトに存在し、データベース書き込みには存在しない
`canShowProcessAction()` は表示のための読み取り側の派生判断です。`ProcessOrder` は書き込み側の不変条件をあらためて検証しなければなりません。BDR は Command モデルを定義しません。Ray.MediaQuery は DML を実行できますが、それを書き込み手段として選ぶかどうかはアプリケーション側の設計です。

これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の区別に従います:
- **Query モデル**は画面や帳票に合わせてデータを形作り、派生・表示の振る舞いを持つことができる
- **Command モデル**はドメインの整合性を守り、業務上の行為を実行してよいかを判断する
- **SQL** は projection に向いている。JOIN、集約、計算、非正規化された結果の形を自然に表せる

### Q: これはCQRSパターンですか?

**A: はい、特にクエリ(読み取り)側です。** BDRパターンはCQRSのクエリ側の強力な実装です。
**A: BDRパターンは CQRS の Query 側に適用できます。ただし、より正確には rich read model のパターンです。** Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。

出発点は単純です。読み取りと書き込みでは、最適なモデルが違います。

書き込み側には、ドメインの整合性を守るモデルが必要です。そこでは意図、振る舞い、不変条件、失敗理由を扱います。「この業務上の行為は実行してよいか?」に答えるモデルです。

読み取り側では、画面、帳票、API レスポンスのために、非正規化されたフラットなデータが欲しいことがよくあります。「今表示するにはどんな形が役に立つか?」に答えるモデルです。同じ Repository や Entity モデルで両方を満たそうとするから無理が生じます。

CQRSは読み取りと書き込みの責務を分離します:
- **クエリ側(BDRパターン)**:ビジネスロジックを含む豊富なドメインオブジェクトによる複雑な読み取り
- **コマンド側**:状態を変更するシンプルで焦点を絞った書き込み
CQRS はしばしば、データベースを分ける、インフラを分ける、Repository の置き場所を分ける、といった物理的なアーキテクチャとして誤解されます。それらは有用な実装上の選択肢になることはありますが、本質ではありません。本質は、Command は業務の意思決定であり、Query は表示のための構造である、という分離です。

BDRパターンは複雑な部分(クエリ)を以下を組み合わせて処理します:
- データ取得のためのSQLの力
- 変換と充実化のためのファクトリー
- ビジネスロジックのためのドメインオブジェクト
BDR は薄い DTO に限定されません。読み取りモデルは、派生・表示のロジックである限り、振る舞いを持てます。合計、ラベル、表示可否、読み取り側の優先度分類など、現在の projection に関する問いに答える振る舞いです。状態変更の不変条件は Command 側に置きます。

一方、コマンド側はシンプルに保たれます:
- 直接的なUPDATE/INSERT/DELETE文
- イベントソーシング(必要な場合)
- 書き込み前のシンプルな検証
SQL はそもそもこの Query 側の性質を持っています。`SELECT` は JOIN、集約、計算を使って、必要な形へ結果を projection できます。その構造を永続的な正規ドメインモデルであるかのように扱う必要はありません。BDR では、SQL ファイルがその projection を定義し、ファクトリやドメインオブジェクトが PHP の型として表面を与えます。

この分離により、両側がよりシンプルで保守しやすくなります
Query モデルは使い捨てであってもかまいません。画面、帳票、API レスポンスが変われば、別の `SELECT` と小さな読み取りモデルを作ればよい。それは DRY 違反ではなく、CQRS の要点です。関心が違うものには、違うモデルを与えます

### Q: ファクトリーで外部APIを呼ぶと、リスト取得時に遅くなりませんか?

Expand Down Expand Up @@ -686,12 +684,15 @@ final class ProductDomainFactory
```php
final readonly class ProductDomainObject
{
private ?float $currentPrice = null;
public function __construct(
private string $id,
private PriceProvider $priceProvider,
) {}

public function getCurrentPrice(): float
{
// 実際に必要な時にのみ取得
return $this->currentPrice ??= $this->priceService->getPrice($this->id);
// 実際に必要な時だけ取得する。キャッシュはreadonlyオブジェクトではなくprovider側で管理する。
return $this->priceProvider->getPrice($this->id);
}
}
```
Expand All @@ -709,7 +710,11 @@ public function getProduct(string $id): ProductDomainObject;

重要なのは、いつどのようにデータをロードするかについて**意図的であること**です。ファクトリーパターンは、この戦略を完全にコントロールする力を与えます。

## 関連レシピ
## BEAR.Sunday 連携

BEAR.Sunday は、アプリケーションの操作を URI で参照できる `ResourceObject` として表す PHP アプリケーションフレームワークです。`#[Embed]` は、それらのリソース間の関係を宣言します。

この境界は BDR と相性が良いです。Repository は **何を**問い合わせるかを宣言したまま、独立したリソースリクエストを **いつ** **どう**実行するかは BEAR.Sunday と BEAR.Async がアプリケーション境界で決めます。Repository interface と SQL file は変わりません。

- [BDR + BEAR.Async: 並列 SQL レシピ](./BEAR_ASYNC_RECIPE-ja.md) — 各リポジトリ呼び出しを `ResourceObject` で包むと、`#[Embed]` がアプリケーション境界でそれらを並列実行する。

Expand Down
Loading
Loading