Query Delegation Pattern
クエリの「構築」と「実行」を分離する、ApexEloquent の設計哲学を解説します。Salesforce 開発で広く採用される Selector Pattern の長期運用課題から出発し、Query Delegation Pattern がなぜそれを解くのか、ApexEloquent がそれをどう体現しているのかをまとめます。
Introduction
Salesforce 開発の現場では、データアクセスを整理する手段として Selector Pattern が広く採用されてきました。クエリをひとつの場所に集めることで、ビジネスロジック側からは生 SOQL を見せず、メソッド呼び出しだけでデータが取れる、というシンプルな利点があります。
しかし、Selector Pattern を中規模以上のプロジェクトで長期運用すると、「ちょっと違うクエリが欲しい」という要求がたびたび発生し、メソッドや引数を追加するパッチが積み重なっていきます。Selector のクラスは膨らみ、当初のシンプルさは少しずつ失われていきます。さらに、テスト自体が DB を必要とするため、結合テストしか書けないという制約もついて回ります。
Selector Pattern の長期運用で起きること
- ユースケースごとに微妙に違うクエリが必要になり、メソッドや引数が継ぎ足されて Selector が肥大化する
- 用途別に Selector を分割すると、似たような Selector クラスとインターフェースが大量に生まれる
- フラグ引数や条件分岐が積み上がり、メソッドの読みやすさと保守性が下がっていく
- あるユースケース向けのクエリ変更が、別の機能に予期しない影響を与える
- テストは Selector に対するモッククラスの実装と DB アクセスを伴う結合テストに偏る
「分離」が必要だと感じた理由
これらの状況に向き合っていくうちに、ひとつの問いに辿り着きました。
「クエリの構築とクエリの実行は、本当に同じ責務なのか?」
どのオブジェクトに、どんな条件でクエリを投げるかを決めるのは、ドメインのルールやユースケースに深く根ざした ビジネスロジックの一部 です。一方、組み立てられたクエリを実際に DB に投げ、結果を返す処理は、純粋な I/O でしかありません。
この 2 つを Selector の中に同居させるのではなく、明確に切り分けたい。そう考えた末に行き着いたのが Query Delegation Pattern という発想です。
Problems with Traditional Patterns
クエリ構築と I/O が混ざることで起きる問題
Selector は典型的に、「何のデータをどう取り出すか」を決めるクエリ構築と、「実際に DB に問い合わせて結果を取り出す」実行処理の両方を抱えます。クエリ構築はビジネスロジックに近く、実行はインフラ層に属する、と整理すると、本来この 2 つは 別の責務 です。
これらが Selector の中に混ざると、徐々に次のような状態に陥っていきます。
- クエリの組み立てが Selector の内側に閉じ込められ、ドメイン側からは「何をどう取りに行っているのか」が見えづらくなる
- クエリの修正が、その Selector を使うすべての機能に波及するリスクを伴う
- テストには Selector インターフェースに対するモック実装が必要で、テストごとに使い分けるためにバリエーションを準備する手間が増える
- ひとつの Selector に処理が集中すると、メソッドを足し続ける運用になり、継続的な機能開発の足を引っ張る
- ユースケースごとに Selector を分割しても、似たようなインターフェースと実装クラスが何個も並ぶことになり、テスト構築への心理的負担が増えていく
理想的には、「どんなデータが必要か」はユースケース側 (ドメイン側) で明示的に表現し、Selector には「実行」だけを委ねる形にしたい。これが Query Delegation Pattern が出発する地点です。
再利用性と文脈の喪失
Selector の魅力のひとつは 再利用性 です。「特定条件で絞り込んでレコードを取得する」メソッドがひとつあれば、複数のユースケースから共通で使えます。
しかしこの再利用性は、しばしば ビジネス上の意図の不明瞭さ と引き換えになります。汎用的に設計された Selector メソッドは、「なぜそのデータが必要なのか」「どんな文脈で使われるのか」という情報を失いがちです。
💭 Selector のメソッドは、汎用的に作るほど「具体的なビジネス意図」から遠ざかっていく。
この問題はテストの観点からも軽視できません。ビジネス意図が曖昧なメソッドは、テストケースを書くときに「何を検証すればよいか」が漠然としやすく、結果としてテストの厚みが不足しがちになります。
What is Query Delegation Pattern?
基本コンセプト
Query Delegation Pattern は、「クエリ構築」と「クエリ実行」の責務を明確に分離するアプローチです。
| 責務 | 担い手 | 性質 |
|---|---|---|
| Query Construction (クエリ構築) | ドメイン側 (Usecase) | ビジネスルールに基づいて条件を組み立てる |
| Query Execution (クエリ実行) | ApexEloquent 内蔵の IEloquent | 組み立て済みクエリを受け取り、DB との I/O を引き受ける |
従来の Selector は両方の責務を抱えていましたが、Query Delegation Pattern では次のように分けます。
- ドメイン側 (Usecase) が、クエリの設計図を組み立てる
- 実行側 (
IEloquent) は、組み立て済みクエリを受け取り、それを実行する
💡 実行側が関心を持つのは「何を取るか」ではなく「どう取るか」だけ、という設計に切り替わります。
ApexEloquent における役割分担
ApexEloquent はこの分離をフレームワークレベルで実現するために、3 つのコア機能を提供します。
| 役割 | クラス | 説明 |
|---|---|---|
| Query Builder | Scribe | クエリの設計図を、型ヒントとメソッドチェーンで組み立てる immutable なビルダー |
| Data Access | IEloquent (Eloquent / MockEloquent) | 設計図を受け取って SOQL を発行する側。本番は Eloquent、テストは MockEloquent を DI で差し替える |
| Record Wrapper | IEntry (Entry / MockEntry) | 取得結果のラッパー。SObject と AggregateResult を同じインターフェースで扱える |
Query Reusability
クエリの再利用をどう扱うか
Query Delegation Pattern では、クエリは基本的にユースケースごとに個別に組み立てる方針を取ります。そのほうがビジネス意図がコードに残るからです。一方で、「このクエリは複数のユースケースから共通で使いたい」という場面も現実には存在します。
ApexEloquent はこの問題に対して、2 つの選択肢 を提供します。チームの規模、ドメインの複雑さ、クエリの共通度合いを見て、「このプロジェクトではどちらに倒すか」を意識的に選びます。
選択肢 1: Usecase 内に閉じ込める
ビジネスロジックのそばで Scribe を組み立て、文脈を完全に保つアプローチ。Apex Stem の Handler-Usecase Architecture ではこちらが原則です。
メリット:
- なぜそのデータが必要なのか、という文脈がコードに残る
- 別のユースケースへの予期しない影響が起きにくい
- ユースケースを読むだけで、必要なデータの全体像が見える
選択肢 2: Selector 派生としての「クエリ保管庫」を持つ
共通化したい Scribe をユーティリティ的なクラスに切り出し、必要なユースケースだけが明示的に取り込むアプローチ。Selector Pattern の派生として、「クエリの部品」を提供するスタイルです。
メリット:
- 重複が大きいクエリを 1 箇所に集約できる
- 共通化したクエリが Scribe オブジェクトとして渡されるので、ユースケース側でさらにチェーンメソッドで条件を継ぎ足せる
- Selector Pattern からの段階的移行が自然に取れる
利用側のイメージ:
// 1. Id で絞り込む基本クエリを保管庫から取得
Scribe oppScribe = OpportunityVault.getById(oppId);
// 2. ユースケース固有の条件を継ぎ足す
oppScribe = OpportunityVault.addNameCondition(oppScribe, '%TestName%');
// 3. 見積を子サブクエリとして追加
List<String> quoteFields = new List<String>{ 'Id', 'Name', 'GrandTotal' };
oppScribe = OpportunityVault.addQuotes(oppScribe, quoteFields);
// 4. 実行は IEloquent に委譲 (Query Delegation)
List<IEntry> entries = this.eloquent.get(oppScribe);
ここでは商談のクエリ保管庫として Vault という単語を使用しています。
保管庫の各メソッドは Scribe を返すだけで、SOQL の発行は最後の IEloquent.get(scribe) に集約されます。これが Query Delegation Pattern と「保管庫」スタイルの両立を成立させる仕組みです。従来の Selector のように 「メソッドの中で SOQL を実行して結果を返す」形だと、ユースケース側から条件を継ぎ足す余地がなく、結局メソッドや引数のバリエーションを増やすしかなくなります。
どちらを選んでも成立する理由
いずれの場合も、Scribe が .field() / .whereEqual() などのメソッドチェーンで クエリを部品として組み立てられる 性質を持っていることが、両方の選択肢を成立させています。ユースケース内で 1 から組むことも、保管庫から取ってきた途中状態の Scribe に対してさらに .whereEqual(...) を継ぎ足すことも、同じ書き味で扱えます。
🎯 どちらが正解、という話ではありません。チームの状況に合わせて選び、必要なら片方からもう片方へ移行することもできます。
Comparison with Traditional Patterns
何がどう変わるのか
| 観点 | Traditional Selector | Query Delegation Pattern |
|---|---|---|
| 責務の置き方 | クエリ組み立てと DB 実行が同居 | ドメイン側がクエリを組み立て、IEloquent は実行のみ |
| 長期運用 | パッチの積み重ねで肥大化、シンプルさを失う | 構造が崩れにくく、追加機能を継ぎ足しやすい |
| テストの書き方 | DB を必要とする結合テスト中心 | MockEloquent 差し替えで DB なしの単体テストが書ける |
| 意図の可視化 | 汎用メソッド化で文脈が失われがち | ユースケースのそばに組み立てがあり、なぜそのデータが必要かが残る |
Implementation in ApexEloquent
ApexEloquent は Query Delegation Pattern をフレームワークレベルで体現します。実装上の主な特徴は以下のとおりです。
動的なクエリ構築
ドメイン側が Scribe でクエリを組み立て、文脈に応じて条件を継ぎ足します。Scribe は immutable なので、共通の Scribe を起点に「先月用」「今期用」など複数のクエリを派生させても、互いに影響しません。
クエリ実行は ApexEloquent 内蔵で済む
実行側は ApexEloquent 内蔵の IEloquent が共通で引き受けます。自前で Selector を量産する必要がなく、組み立てた Scribe を渡すだけで SOQL が発行されます。
モック差し替えで DB なし単体テスト
構築と実行が分かれているので、IEloquent を MockEloquent に差し替えるだけで DB なしの単体テストが書けます。MockEloquent には upsertedRecordsAt(label) / deletedCountAt(label) といった Spy もあり、何が DML されたかをアサートできます。
書き込み不可項目もモック可能
数式項目・ロールアップ・親リレーション・auto-number など、通常は書き込み不可な項目も MockEntry 側で自由に値を入れられます。これにより、本番では計算結果でしか取れない値も、テストでは「この値が返る前提でロジックを検証する」という形で扱えます。
実装の入口
実装の入り口としては、まず Scribe でクエリを組み立てる を、続いて データ取得と DML、IEntry、Mock を読むと、Query Delegation の実コードへの落とし込みがそのまま見えます。
Summary
Query Delegation Pattern が解くもの
Query Delegation Pattern は、Selector が抱えがちな「クエリ構築」と「DB 実行」の責務の混在を、根本から分けてしまうアプローチです。
主な効果:
- 責務の明確な分離による、長期運用での保守性向上
- 複雑なモック実装を書かずに済むテスト構築の手軽さ
- クエリ意図の可視化によるドメインモデルの健全化
- ビジネスロジックとデータアクセス層の結合度低下
Query Delegation Pattern は単なる技術的な工夫ではなく、データアクセス層の設計に対する考え方そのものを、よりクリーンな方向に押し出すための哲学です。
関連ドキュメント
- ApexEloquent トップ: Query Delegation Pattern を採用した OSS の全体像
- Apex Stem: ApexEloquent を含む 4 OSS と Handler-Usecase Architecture の組み合わせ
- Apex における Repository パターンの試行錯誤と内蔵化: なぜ Repository を ApexEloquent に内蔵したか (Selector Pattern との関係)
- Apex の生 SOQL から、チェーンメソッドで組み立てる ORM へ: 生 SOQL から Scribe への移行入門
- MockEntry: Apex のテストデータ作成を成立させる仕組み: モック側の Deep Dive
- モックテストの偽陽性を検知する: SELECT 漏れの安全網: Scribe との連携で SELECT 漏れを単体テストで検知
- Handler-Usecase Architecture: Query Delegation を実コードへ落とし込む場所 (Usecase 層)
- ApexEloquent ガイド: Scribe / IEloquent / IEntry の使い方と API リファレンス