Repositoryって本当に必要?
最初は「Repositoryなんて回りくどいだけでは?」と思ってました。
Eloquentで Spot::query()->where(...)->get() って書けば出るし、早い。
でも、プロジェクトが育ってくると地味な困りごとが増えます。
- クエリがController/UseCase/ViewModelのあちこちに出現
- 条件が微妙に違う“ほぼ同じ”クエリが乱立
- テストでDB前提になって重い・遅い・壊れやすい
- 将来の差し替え(キャッシュ・検索エンジン・別DB)に弱い
このへんを一枚の“境界”で受け止めるのがRepositoryでした。
「抽象化したいからする」ではなく、雑音を減らすためにする、という感触。
目次
Repositoryって何者?
ざっくりいうと、「取得・保存などの永続化アクセスの入口を、用途名で固定する」層。
Eloquentや生SQLの書き方ではなく、「何を取りたいのか」の名前で呼びます。
interface SpotListQuery {
public function search(SpotSearchCondition $cond): SpotListResult;
}
final class DbSpotListQuery implements SpotListQuery {
public function search(SpotSearchCondition $cond): SpotListResult {
// Eloquent or QueryBuilder で実装
// (JOIN/集計/ページングなど含む)
}
}UseCase側から見ると、クエリの“意味”だけが見える。
実装は後ろに隠れる。これが想像以上に効きます。
いらないパターンは?
- CRUDだけの小さい画面(典型的な管理画面)
- 集計もJOINもなく、単純な1テーブルアクセス
- 将来の置き換えや再利用の予定がない
- テストもFeature中心で、DBを使うのがむしろ素直
この辺は無理にRepositoryを作らないほうが読みやすいみたいです。
“最小”で始めて、痛みが出たら導入でOK。
いつ効いてくる?
- 同じ取得ロジックを別画面で何度も使う(ロジックの重複を止めたい)
- 検索条件が複雑(JOIN/集計/条件の組み合わせが増殖)
- テストでDB依存を減らしたい(Stub/Fakeに差し替えたい)
- 将来の差し替え余地(Elasticsearch/キャッシュ/外部APIに置き換える可能性)
つまり、再利用・テスト・将来の自由度がキーワード。
ここでRepositoryが効いてくる。
QueryとRepository
検索や一覧などの取得はクエリ系、保存・更新をするのは
コマンド系として使い分けるみたいです。
Repositoryひとつに読み込みも保存も全部詰め込みは
事故が起きがちらしいので、
読み取りはQueryオブジェクトに分けるとクリアみたいですね(CQRSっぽく)
具体例(読み取りは Query、書き込みは Repository)
読み取り
// Application 層の“契約”
interface SpotListQuery {
public function search(SpotSearchCondition $cond): SpotListResult; // DTOで返す
}
// Infrastructure 実装
final class DbSpotListQuery implements SpotListQuery {
public function search(SpotSearchCondition $cond): SpotListResult {
$q = \DB::table('spots')
->join('prefectures', 'prefectures.id', '=', 'spots.prefecture_id')
->when($cond->keyword !== '', fn($q) =>
$q->where('spots.name', 'like', '%'.$cond->keyword.'%')
)
->orderByDesc('spots.id');
// ページング・DTO変換…
return new SpotListResult(/* … */);
}
}書き込み
interface SpotRepository {
public function save(Spot $entity): void; // 永続化
public function delete(int $id): void;
public function findById(int $id): Spot; // Entityで返すことも
}UseCase側
final class SearchSpotUseCase {
public function __construct(private SpotListQuery $query) {}
public function handle(SpotSearchCommand $cmd): SpotListViewModel {
$result = $this->query->search(SpotSearchCondition::from($cmd));
return SpotListViewModel::fromResult($result);
}
}これで責務がわかれてスッキリし、テストもしやすいですね。
ありがちな失敗と回避
なんでもかんでも1つのRepositoryに集約 → “神Repository化”
→ 読み取り用のQueryに分割。名前を“使い道”で付ける
DTOを返さずModelのまま返す → 上位層がEloquentに縛られる
→ DTO/Row/Resultで返す(層間の契約にする)
抽象化が早すぎる → 小規模ではむしろ読みにくい
→ 最初は直書き、痛みが出た箇所だけRepository化で十分
まとめ
結局のところ、Repositoryって「必ず要るもの」ではないんですよね。
小さいうちはEloquent直書きで十分。
でも、クエリが散らばったり、テストが重くなったりしたとき、その“境界”が欲しくなる。
Repositoryを挟むと、「どう取るか」ではなく
「何をしたいか」で呼べるようになる。
それだけでコードが静かになるし、
テストも差し替えで動かせるようになる。
いまの自分にとっての答えは、
Repositoryはきれいにするための層じゃなくて、雑音を減らすための線。
使うことで、責務がはっきりして保守性が高まる感じがしますね。
今回は以上です!