ひとことで言うと#
ドメインモデル(集約)の保存・取得を抽象化し、永続化の詳細(SQL、ORM等)をドメインロジックから隠蔽することで、ビジネスルールの純粋さとテスタビリティを確保するパターン。
押さえておきたい用語#
- Repository(リポジトリ)
- 集約の保存・取得・削除を担うインターフェースのこと。ドメイン層に定義し、インフラ層で実装する。
- Aggregate(アグリゲート)
- トランザクション整合性を保つドメインオブジェクトの境界単位を指す。リポジトリは集約単位でアクセスする。
- Aggregate Root(集約ルート)
- 集約の外部からの唯一のアクセスポイント。リポジトリが返すのは常に集約ルート。
- Unit of Work
- トランザクション内の変更を一括でコミットする仕組み。リポジトリと組み合わせて使う手法である。
- In-Memory Repository
- テスト用にメモリ上で動作するリポジトリ実装。DBなしでドメインロジックをテストできる。
リポジトリパターンの全体像#
こんな悩みに効く#
- ドメインロジックの中にSQLが混在して読みにくい
- DB接続が必要でドメインロジックのユニットテストが書けない
- ORMの変更やDB移行の影響がビジネスロジックに波及する
基本の使い方#
OrderRepositoryはOrder集約全体(OrderItem、ShippingInfo等を含む)を一括で保存・取得する。Entityごとにリポジトリを作るのは間違い。findById(id), save(aggregate), delete(id) のようなメソッドを定義する。インターフェースのメソッド名はドメインの言葉で表現し、SELECT * FROMのようなDB用語は使わない。PostgresOrderRepositoryはSQL/ORMで永続化し、テスト用のInMemoryOrderRepositoryはMapやListで動作する。DIコンテナで環境に応じて実装を切り替える。具体例#
エンジニア35名のEC。注文ドメインのテストがすべてDB接続を必要とし、CI実行に 22分 かかっていた。テストの追加を避けるエンジニアも出始めていた。
OrderRepositoryインターフェースをドメイン層に定義し、PostgresOrderRepository(本番用)とInMemoryOrderRepository(テスト用)を実装。
テスト時はInMemory実装を注入する形に変更。ドメインロジックのユニットテスト実行時間が 22分 → 2分 に短縮。テストカバレッジが 45% → 78% に向上し、バグの早期発見率も改善した。
エンジニア50名のBtoB SaaS。MongoDBの運用コストが高騰し、PostgreSQLへの移行を検討。しかし、MongoDBのクエリがサービス層に散在しており、移行の影響範囲が推定 200ファイル に及んだ。
リポジトリパターンでリファクタリング。各集約にRepositoryインターフェースを定義し、MongoDB実装をAdapter化。その後、PostgreSQL Adapterを新規実装。
Feature Flagで段階的に切り替えた結果、ドメインロジックの変更は ゼロファイル。移行期間は当初見積もり6ヶ月から 8週間 に短縮された。
エンジニア15名の医療系スタートアップ。患者カルテの集約が複雑で、1つのカルテに診療記録・処方箋・検査結果が紐づく。ORMのリレーション設定が複雑化し、N+1問題やデータ不整合が月 8件 発生していた。
PatientRecordRepositoryを集約ルート単位で設計し、集約全体を1回のクエリで取得するカスタムSQLを実装。ORMのリレーション自動読み込みに頼らず、リポジトリ内で明示的にデータを組み立てる方式に変更。
N+1問題が ゼロ に。データ不整合も 月8件 → 月0.5件 に減少。リポジトリ内にSQLが集約されたことで、パフォーマンスチューニングも1箇所の修正で完結するようになった。
やりがちな失敗パターン#
- Entityごとにリポジトリを作る —
OrderRepositoryとOrderItemRepositoryを分けると、集約の整合性を呼び出し側が管理する必要が出る。集約ルート単位で1つ - リポジトリのインターフェースが肥大化する —
findByNameAndStatusAndCreatedAtBetweenのようなメソッドが増殖する。仕様パターン(Specification)を使って検索条件を分離する - リポジトリ内でドメインロジックを実行する — WHERE句で複雑な条件判定をするのはドメインロジックの漏洩。取得後にドメインモデル内で判定する
- InMemory実装とDB実装の振る舞いが異なる — InMemoryでは通るがDB実装で落ちるテストは信頼性が低い。重要なケースは統合テストでDB実装も確認する
まとめ#
リポジトリパターンは集約の永続化を抽象化し、ドメインロジックをDBの技術詳細から分離するDDDの基本パターン。ドメイン層にインターフェースを定義し、インフラ層にDB実装、テスト層にInMemory実装を配置する。集約ルート単位でリポジトリを設計することで整合性を担保し、DIによる実装の差し替えでテスタビリティとDB移行の柔軟性を同時に確保する。