リポジトリパターンとは、ドメインオブジェクトの保存や取得を、コレクションを扱うようなインターフェースで表す設計パターンです。
一言でいうと
リポジトリは、アプリケーションの中心からDBやORMの詳細を隠すための窓口です。
ユースケースは「注文を保存する」「ユーザーをIDで探す」と言えればよく、SQL、ORM、外部API、キャッシュの詳細を知る必要はありません。
登場人物
| 名前 | 役割 |
|---|---|
| ドメインモデル | 保存したい業務オブジェクト |
| Repositoryインターフェース | 保存/取得の約束 |
| Repository実装 | DBやORMを使った実装 |
| ユースケース | Repositoryを使って処理を進める |
Repositoryは、DBテーブルを直接表すものではなく、ドメインから見た保存・取得の入口です。
基本形
interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
ユースケースはこのインターフェースだけに依存します。
class CancelOrderUseCase {
constructor(private readonly orders: OrderRepository) {}
async execute(orderId: string): Promise<void> {
const order = await this.orders.findById(orderId);
if (!order) throw new Error("order not found");
order.cancel();
await this.orders.save(order);
}
}
ここでは、DBが何か、ORMが何か、SQLがどう書かれているかをユースケースは知りません。
ORMとの違い
| 観点 | ORM | Repository |
|---|---|---|
| 主な目的 | DB行とオブジェクトの変換 | 保存/取得の抽象化 |
| 関心 | テーブル、クエリ、リレーション | ドメインから見た操作 |
| 置き場所 | インフラ層寄り | インターフェースは内側、実装は外側 |
ORMを使っていてもRepositoryは作れます。Repository実装の中でORMを使う形です。
何がうれしいのか
- ユースケースからDB詳細を隠せる
- テストでインメモリ実装に差し替えやすい
- 保存方法を変えても中心の処理が壊れにくい
- クエリの意図をメソッド名で表現できる
たとえば findActiveUsersForBilling() のような名前にすると、SQLの詳細より先に業務上の目的が伝わります。
よくあるアンチパターン
すべてのテーブルに機械的にRepositoryを作る
DBテーブルごとに薄いCRUD Repositoryを作るだけでは、抽象化の意味が薄くなります。ドメインやユースケースから見た保存単位で考えます。
Query Builderをそのまま外へ漏らす
Repositoryの戻り値がORMのQuery Builderになると、呼び出し側がDB詳細に依存します。必要な結果を返すメソッドに閉じ込めます。
業務ルールをRepositoryに入れる
Repositoryは保存と取得が責務です。キャンセル可否、割引計算、状態遷移などはドメインやユースケースへ置きます。
向いている場面
| 向いている | 向いていない |
|---|---|
| ドメインモデルを守りたい | 単純なCRUDだけ |
| DB実装を隠したい | 小さなプロトタイプ |
| テストで差し替えたい | ORMの機能を直接使うだけで十分 |
| 複雑な検索意図がある | 画面専用の単純な一覧 |
参照専用の複雑な検索は、RepositoryではなくQuery ServiceやRead Modelへ分けることもあります。
まとめ
リポジトリパターンは、保存や取得の詳細をアプリケーションの中心から隠すための設計です。
RepositoryはDBテーブルのコピーではなく、ドメインから見たデータアクセスの約束です。業務ルールとDB都合を混ぜないために使います。
参考リソース
- Martin Fowler: Repository
- Microsoft: Infrastructure persistence layer design
- Microsoft: Implementing repository and unit of work patterns