1. はじめに
Repositoryパターンの恩恵とその限界
データアクセスの責務を整理し、ビジネスロジックからデータベースの操作を隠蔽するために広く使われているのが「Repositoryパターン」です。
リポジトリを導入することで、SQLやクエリ言語への依存を減らし、テストしやすいコードを実現できるのは大きなメリットです。
しかし、現実の開発ではこのパターンにも限界が存在します。
リポジトリが「データの取得ロジック(クエリ構築)」と「データベースへのアクセス(実行)」の両方を担ってしまうことで、次第に以下のような問題が発生しがちです。
- ユースケースごとに微妙に異なるクエリを追加するたび、リポジトリが肥大化していく
- もしくは、ユースケースごとにリポジトリを作成しリポジトリ(とインターフェース)が大量に作られる
- 様々なクエリに対応するため、フラグ引数や条件分岐が増え、メソッドの読みやすさ・保守性が低下する
- 特定ユースケースに依存したクエリ変更が、他の機能にも思わぬ影響を与える可能性がある
- 本来ドメインオブジェクト内に閉じ込めるべき情報がリポジトリに流出し、オブジェクト指向設計が崩れる
「リポジトリを使っているのに、なんだか恩恵をうまく受けられない」という経験をしたことはないでしょうか?
なぜ「分離」が必要と感じたのか?
こうした課題に私自身が何度も直面し、ある疑問を持つようになりました。
「そもそも、クエリの構築と、データベースへの実行は、同じ責務なのだろうか?」
取得対象のオブジェクトや条件を決めるクエリ構築は、ドメインのルールやユースケースに深く関わる「業務ロジック」の一部だと私は考えます。
一方で、クエリを実際にデータベースへ送り、データを取ってくる実行は、ただのI/O処理に過ぎません。
この二つを一つのリポジトリに押し込めるのではなく、明確に分離するべきではないか?
そう考えた結果たどり着いたのが、Query Delegation Pattern(クエリ委譲パターン)というアプローチでした。
本記事では、このQuery Delegation Patternの設計思想と具体的な効果について詳しく紹介していきます。
従来のRepositoryパターンの利点を生かしつつ、さらに責務を明確化し、ドメインモデルの健全性を高める方法を探っていきましょう。
2. 従来のRepositoryパターンが抱える課題
クエリ構築とI/Oの混在による問題
一般的なRepositoryパターンでは、リポジトリクラスが「どのデータを取得するか決めるクエリの構築」と「データベースへの問い合わせを実行する処理」の両方を担います。
しかし、クエリ構築はビジネスロジックに直結し、I/Oはインフラストラクチャ層に属する、という観点から見ると、これらは本来異なる責務を持つ処理です。
この二つがリポジトリに混在していると、次第に様々な問題が噴き出してきます。
- クエリの組み立て方がリポジトリ内部に閉じ込められ、ドメインオブジェクトの意図やコンテキストが見えにくくなる
- クエリの変更に伴う影響範囲が広がり、リポジトリを介したすべての機能に修正が波及するリスクが増す
- 単体テストが全てにおいて実施されていれば問題はないが、そうでない場合は・・・
- テストにおいて、リポジトリのインターフェースを実装したMockクラスを作る必要がある。
- 一つのリポジトリに処理が集中している場合、実装すべきメソッドが大量に出てきて継続的な開発には支障をきたす
- ユースケースごとにリポジトリが分かれている場合、同じようなリポジトリインターフェースやそれを実装したクラスを作成する必要があり、テスト構築の心理的に大きな壁となる
本来、ドメイン側(ユースケース側)が「どんなデータが欲しいか」を自分で明示し、それをリポジトリに「実行だけ」依頼するほうが、設計の健全性は高まるはずです。
再利用性 vs コンテキストの喪失
Repositoryパターンの魅力の一つは、再利用性の高さです。
たとえば、「特定の条件で絞り込んだレコードを取得するメソッド」がリポジトリに一つ用意されていれば、複数のユースケースで共通して利用できます。
しかし、これには大きな落とし穴もあります。
リポジトリに「共通の取得ロジック」を集約すればするほど、各ユースケースごとの細かいコンテキスト(たとえば「この取得は論理削除済みレコードも対象にする」「こちらはしない」など)が埋没してしまうのです。
コンテキストが失われた状態でリポジトリを修正すると、他のユースケースに副作用を与えてしまうリスクが非常に高くなります。
つまり、再利用性の裏には「コンテキスト喪失」という重大なトレードオフが潜んでいるのです。
ロジック肥大化とフラグ地獄
さらに、従来型のリポジトリを運用していくと、次のようなパターンに陥りがちです。
- 「この場合だけ、さらにこの条件を追加したい」というニーズが発生
- リポジトリのメソッドにフラグ引数を追加し、条件分岐を増やす
- 引数やロジックが複雑になり、可読性・保守性が急速に悪化
最終的には、もはやどのユースケースのためにどの分岐が存在しているのか分からない状態になり、修正も怖くなっていく──そんな未来が待っています。
3. Query Delegation Patternとは?
「構築」と「実行」の責務を明確に分離
Query Delegation Patternでは、従来のRepositoryパターンで混在していた「クエリ構築」と「データベースI/O」の責務を完全に分離します。
具体的には、
-
クエリをどう組み立てるかは、ユースケース(ドメイン側) で記述し
-
組み立てたクエリの「実行だけ」を、Repositoryに依頼します。
この分離によって、
- ユースケース側が「どんなデータが欲しいか」を完全に制御できる
- Repository側は「渡されたクエリを素直に実行するだけ」のシンプルな責務になる
- Repositoryが「ただ実行するだけ」のクラスとなるので全てのユースケースにおいて共通で使用することができ、いくらユースケースが増えても新しいインターフェースは必要ない
という大きなメリットが得られます。
Query Delegation Patternにおける役割分担は、次のように整理できます。
設計イメージ
レイヤー | 主な責務 |
---|---|
ドメイン / ユースケース層 | 必要なクエリを組み立てる |
インフラストラクチャ層 (Repository) | クエリを受け取り、データベースへ問い合わせを行う |
これにより、ドメイン層はインフラへの過剰な依存から解放され、クリーンなビジネスロジックを記述できるようになります。
「Queryの再利用」はどうする?
Query Delegation Patternでは、基本的にクエリはユースケースごとに個別に組み立てます。
ただし、「このクエリはどのユースケースでも共通で使いたい」という場合も当然出てきます。
そのときは、
- 再利用したいクエリだけをtrait(あるいはutilityクラス)化しておき
- 必要なユースケースだけが明示的にそれを使用する
という方法をとります。
コンテキストを意識せずに雑に再利用する のではなく、
「このクエリは本当にコンテキストをまたいでも問題ないか?」を設計段階で吟味する
──これがQuery Delegation Patternにおける再利用設計のポイントです。
4. 比較:Repositoryパターン vs Query Delegation Pattern
比較表で見る設計思想の違い
ここでは、従来のRepositoryパターンとQuery Delegation Patternを、主要な観点で比較してみます。
比較項目 | 従来型Repositoryパターン | Query Delegation Pattern |
---|---|---|
クエリ構築の責務 | Repositoryが担当する | ドメイン / ユースケースが担当する |
データベース操作の責務 | Repositoryが担当する | Repositoryが担当する(※クエリの実行のみ) |
再利用性 | 高い(が、コンテキストが消失しやすい) | 低い(必要なら明示的に再利用設計) |
コンテキスト意識 | 低くなりやすい(汎用性を優先) | 高い(ユースケースごとに最適化) |
テスト容易性 | 低い(リポジトリ肥大化 or 大量リポジトリ作成) | 高い(一つのリポジトリのみでOK) |
変更時の影響範囲 | 広い(共有ロジックのため波及しやすい) | 狭い(ユースケース内に閉じる) |
典型的な問題例 | ロジック肥大化、フラグ地獄 | 再利用の工夫が求められる |
従来型Repositoryパターンのメリット・デメリット
メリット
- 同じクエリロジックを複数の場所から使い回せるため、コード量を削減できる
- リポジトリを通してデータ取得するため、一見「きれいに層が分かれている」ように見える
デメリット
- コンテキスト(=「なぜこのデータを取得したいのか?」という背景)が失われやすい
- 「ちょっとだけ違う取得」をしようとするたびに、フラグ引数や条件分岐が増え、リポジトリの複雑度が爆発する
- 汎用化を重視するあまり、ビジネスロジックがブラックボックス化しやすい
Query Delegation Patternのメリット・デメリット
メリット
- クエリの意図がユースケースに明示されるため、コードの可読性と保守性が高い
- 「取得」と「実行」が明確に分かれるため、テストが容易になる
- データ取得部分のI/Oを完全に抽象化できる
デメリット
- クエリを毎回ユースケースごとに組み立てるため、コードが若干冗長に見えることがある
- うまくクエリ再利用を設計しないと、同じようなクエリを何度も書くリスクがある
- 設計者・レビュアーにコンテキスト意識と設計力が求められる
5. Query Delegation Patternの実装例
手前味噌ですが、このQuery Delegation Patternを使用する私が作成したApex用のDMLフレームワーク「Apex Eloquent」の実装例を紹介します。
/**
* Updates the name of the Opportunity record with the given Id.
*/
public Opportunity execute() {
// Get the Opportunity record
Query query = (new Query())
.source(Opportunity.getSObjectType())
.pick(new List<String>{'Id', 'Name'})
.condition('Id', '=', oppId);
Opportunity opp = (Opportunity) this.repository.first(query);
// Update the Opportunity name
opp.Name = 'Updated Opportunity Name';
// Update the record in the database
Opportunity updatedOpp = (Opportunity) this.repository.doUpdate(opp);
return updatedOpp;
}
まず、クエリの構築部分が次のようになっています。
// Get the Opportunity record
Query query = (new Query())
.source(Opportunity.getSObjectType())
.pick(new List<String>{'Id', 'Name'})
.condition('Id', '=', oppId);
続いて、データベースに問い合わせて実際にデータを取得する部分がこちらです。
Opportunity opp = (Opportunity) this.repository.first(query);
このように、「構築」と「実行」が完全に分離しています。
そして
this.repository
このRepositoryクラスは
- 実際にDML操作を行うRepository
- Mockとしてテストクラスで指定したエンティティを返す
という2つだけしか存在しておらず、これらを差し替えることでユニットテストからI/Oを切り離し、素早いテストが可能になるのです。
6. まとめと今後の展望
実際に導入してみた感想
私のプロジェクトでは「ユースケースごとのリポジトリパターン」を採用していたのですが
- ユースケースごと/エンティティごとにリポジトリクラスを作成していたためクラスがたくさん必要でプロダクションコードも、テストコードも書くのが苦痛
だったのですが、共通のRepositoryでデータアクセスのみを担当させ、ロジックをドメインに寄せることでこれらを一気に解決でき非常に開発体験が向上しました。
特にApexではnamespace
の概念がないため、クラスを大量に作る必要がなくなるのはかなり大きいです。
しかし、LaravelのEloquentやRailsのActive Recordはクエリの「構築」と「実行」を一緒に行う設計になっているので、この考えを導入するには
- 実行部分だけをラップするRepositoryクラスの用意
- 開発メンバーへのメリット / デメリットの説明とプロダクション / テストコードの書き方指南
が必要になってきます。ただしこれらを乗り越えればさらに良い開発体験が待っていると私は確信しています。