Repositoryって本当に必要?

2025.10.17 09:00
2025.10.21 20:49
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はきれいにするための層じゃなくて、雑音を減らすための線。
使うことで、責務がはっきりして保守性が高まる感じがしますね。

今回は以上です!