Apex の生 SOQL から、チェーンメソッドで組み立てる ORM へ
長くなりがちな SOQL 文字列、扱いにくいリレーション、遅くて壊れやすい DB 依存のテスト。Salesforce 開発者が日常的に向き合っているこれらの課題は、Apex の標準的な書き方をそのまま使っている限り、なかなか減りません。
ApexEloquent は、メソッドチェーンでクエリを宣言的に組み立て、テストでは DB に触れずに完結できる ORM を提供します。生 SOQL 中心の Apex 開発から、どこがどう変わるかを順に見ていきます。
生 SOQL 中心の Apex 開発で起きること
標準の SOQL は強力ですが、運用していると次のような問題が必ず顔を出します。
- ❌ 可読性の限界: 手で連結した長い SOQL 文字列は、すぐに保守不能になる
- ❌ 動的条件の組み立てが面倒:
if分岐の中で条件を追加するには、文字列操作の組み合わせを書くことになる - ❌ 型安全性が無い: 文字列内のフィールド名のタイポは実行時まで気付けない
- ❌ リレーションが扱いにくい: 親子のリレーション名 (
__r付きの名前など) を正確に覚えていないと書けない - ❌ テストが書きづらい: ビジネスロジックが DB に強く結合し、テストごとに大量のデータを
insertする必要が出る
これらは「生 SOQL を書く」という選択をした瞬間に、ほぼ自動的についてくる代償です。ApexEloquent はこの代償を払わずに同じ仕事をするために生まれました。
ApexEloquent によるアプローチ
ApexEloquent は、Laravel の Eloquent から発想を借りた Apex 向けの ORM フレームワークで、2 つの主要コンポーネントが軸になります。
Scribe: SOQL クエリをメソッドチェーンで組み立てる、immutable なクエリビルダーIEloquent(Eloquent/MockEloquent): 組み立てたScribeを受け取って実行する側。本番はEloquent、テストはMockEloquentを DI で差し替える
この「クエリ構築」と「クエリ実行」を分けて設計するアプローチには、独自の設計哲学があります。詳しい背景は Query Delegation Pattern と、なぜ Repository を ApexEloquent に内蔵したかは Apex における Repository パターンの試行錯誤と内蔵化 で扱っています。
このドキュメントでは、設計哲学はいったん置いておき、生 SOQL からの移行で「実際にコードがどう変わるか」 を中心に見ていきます。
基本的な使い方
クエリの組み立ては直感的です。Scribe のメソッドを連ねるだけで、SELECT・WHERE・並び替えがそのまま型付きの API として書けます。
// Industry が 'Technology' のすべての Account の Id と Name を取得するクエリ
Scribe accountScribe = Scribe.of(Account.class)
.fields(new List<String>{ 'Id', 'Name' })
.whereEqual('Industry', 'Technology')
.orderBy('Name');
これと等価な生 SOQL は次のとおりです。
SELECT Id, Name FROM Account WHERE Industry = 'Technology' ORDER BY Name
短い 1 行ならどちらも同程度に読めますが、条件・並び替え・サブクエリが増えるほど、Scribe 版は 構造のまま読めること のメリットが効いてきます。組み立てた Scribe を実行する側 (IEloquent.get(scribe) 等) や、Scribe の API 全体については Scribe でクエリを組み立てる と データ取得と DML、IEntry、Mock を参照してください。
動的にクエリを組み立てる
Scribe が immutable であることが効いてくる代表的な場面が、「ユースケースの条件によってクエリを分岐させたい」ケースです。
Scribe oppScribe = Scribe.of(Opportunity.class)
.fields(new List<String>{ 'Name', 'StageName' })
.whereIn('StageName', new List<String>{ 'Prospecting', 'Qualification' });
// 特定条件のときだけ WHERE 条件を追加する
if(includeHighValueDeals) {
oppScribe = oppScribe.whereGreaterThan('Amount', 100000);
}
// 特定条件のときだけ SELECT するフィールドを追加する
if(includeAmountDetails) {
oppScribe = oppScribe.field('Amount').field('Probability');
}
Scribe のメソッドは毎回新しい Scribe を返すので、元のクエリを壊さずに「条件を足す」「フィールドを足す」を同じ感覚で扱えます。
ここでは SELECT 項目の追加も含むため
ifで書いていますが、WHERE 条件だけを出し入れするならignoreWhenを使うと分岐なしの 1 本のチェーンで書けます (whereIn('Id', ids).ignoreWhen(ids.isEmpty()))。
順番に依存しない構造
上の 2 つの if ブロックは 入れ替えても出来上がる SOQL が同じ になります。field と where のどちらを先に追加しても、最終的に生成されるクエリの SELECT 句・WHERE 句に過不足なく反映されるからです。これは「ユースケース側の判断のしやすい順に if を並べる」自由度をそのまま許してくれます。
同じことを文字列の SOQL でやろうとすると、SELECT 句用の文字列と WHERE 句用の文字列を別の変数で保持して、最後に決まった順序で連結する、という ビジネスロジックとは関係のない処理 が増えてきます。
// 文字列 SOQL で同じことをやる場合 (悪い例)
String selectClause = 'SELECT Name, StageName';
String whereClause = " WHERE StageName IN ('Prospecting', 'Qualification')";
if(includeHighValueDeals) {
whereClause += ' AND Amount > 100000';
}
if(includeAmountDetails) {
selectClause += ', Amount, Probability';
}
String soql = selectClause + ' FROM Opportunity' + whereClause;
SELECT 句と WHERE 句を別変数で持つ書き方が「分岐ごとにどちらの変数を触るか」を間違えるリスクを抱えますし、' AND ...' の先頭スペースを入れ忘れて WHERE Foo = 'X'AND Bar = 'Y' のように繋がってしまう 空白の入れ忘れケアレスミス も典型的です。Scribe ベースの組み立てでは、これらの「クエリ文字列を組み立てるためだけのコード」が一切要らなくなります。
このメソッドチェーン方式の良さは、「クエリを部品として組み立てる」感覚がそのままコードに乗ること です。ユースケース固有の条件を、文字列ではなく型付きのメソッド呼び出しとして表現できます。
リレーションも直感的に扱える
親子のリレーション名 (__r 付きの名前など) を正確に覚えていなくても、Scribe のメソッドで親子クエリを組めます。
// Account と、その配下の Contact を一緒に取得
Scribe accountScribe = Scribe.of(Account.class)
.field('Name')
.withChildren(
Scribe.asChild(Contact.class)
.fields(new List<String>{ 'LastName', 'Email' })
)
.whereEqual('Id', someAccountId);
IEntry accountEntry = (new Eloquent()).first(accountScribe);
List<IEntry> contacts = accountEntry.getChildren('Contact');
親項目を SELECT する parentField、親条件で絞る parentCondition、Junction Object を経由する多対多の through などについては、親項目・子サブクエリ・多対多 で詳しく扱っています。
DB なしテストへの入口
ApexEloquent のもう一つの大きな価値は、IEloquent を MockEloquent に DI で差し替えるだけで、DB を介さない単体テスト が書けることです。
// 想定するクエリ結果をメモリ上に組み立てる
IEloquent mockEloquent = new MockEloquent(new List<IEntry>{
MockEntry.of(Opportunity.class)
.set('StageName', 'Closed Won')
.setParent('AccountId',
MockEntry.of(Account.class).set('Type', 'Prospect')
)
});
// DB に触れず、サービスクラスを直接テスト
AccountService service = new AccountService(mockEloquent);
Account updatedAccount = service.updateAccountType('006...');
Assert.areEqual('Customer', updatedAccount.Type);
MockEloquent / MockEntry の詳しい使い方 (Spy プロパティ、failOn*、親子・集計のモック、SELECT 漏れ検知など) は データ取得と DML、IEntry、Mock で網羅しています。
生 SOQL vs ApexEloquent 比較
| 観点 | 生 SOQL | ApexEloquent |
|---|---|---|
| 可読性 | 複雑なクエリで急速に悪化 | ✅ メソッドチェーンで構造が見える |
| 動的 WHERE | 文字列連結を手で書く | ✅ チェーンメソッドで安全に継ぎ足せる |
| リレーション | リレーション名を覚えて記述 | ✅ withChildren / parentField で直感的 |
| テスト | ❌ DML + テストデータが必須 | ✅ MockEloquent で DB なしテスト |
| 安全性 | フィールド名タイポは実行時エラー | ✅ テスト段階で SELECT 漏れも検知 |
まとめ
生 SOQL を書き続けることで支払っている代償 (可読性 / 動的条件の面倒さ / 型安全性の欠如 / リレーションの扱いづらさ / テストの重さ) は、別々の問題に見えて、「文字列でクエリを書いている」という選択そのものが根に近い 問題です。
Scribe でクエリを組み立て、IEloquent に実行を委ねるスタイルに切り替えると、これらの代償がまとめて軽くなります。生 SOQL を 1 行も書かない、というほど極端な話ではなく、ユースケースに紐づくクエリを Scribe で構造的に組み立て、保管庫に置く共通クエリも Scribe で部品化していく、というのが現実的な移行像です。
関連ドキュメント
- Query Delegation Pattern: なぜクエリ構築と実行を分けるのか
- Apex における Repository パターンの試行錯誤と内蔵化: Repository を内蔵した動機
- Scribe でクエリを組み立てる: Scribe API のガイド
- データ取得と DML、IEntry、Mock: IEloquent / MockEloquent のガイド
- 親項目・子サブクエリ・多対多: リレーション操作の典型例
- ApexEloquent トップ: OSS の全体像