Apex の生 SOQL から、チェーンメソッドで組み立てる ORM へ

Apex Stem ドキュメント
ApexSOQLApexEloquentClean Code
生 SOQL 中心の Apex 開発で支払っている代償 (可読性 / 動的条件 / 型安全性 / リレーション / テスト) を、Scribe + IEloquent でどう軽くするかを実例ベースで解説します。

長くなりがちな 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 として書けます。

APEX
// Industry が 'Technology' のすべての Account の Id と Name を取得するクエリ
Scribe accountScribe = Scribe.of(Account.class)
    .fields(new List<String>{ 'Id', 'Name' })
    .whereEqual('Industry', 'Technology')
    .orderBy('Name');

これと等価な生 SOQL は次のとおりです。

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 であることが効いてくる代表的な場面が、「ユースケースの条件によってクエリを分岐させたい」ケースです。

APEX
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 が同じ になります。fieldwhere のどちらを先に追加しても、最終的に生成されるクエリの SELECT 句・WHERE 句に過不足なく反映されるからです。これは「ユースケース側の判断のしやすい順に if を並べる」自由度をそのまま許してくれます。

同じことを文字列の SOQL でやろうとすると、SELECT 句用の文字列と WHERE 句用の文字列を別の変数で保持して、最後に決まった順序で連結する、という ビジネスロジックとは関係のない処理 が増えてきます。

APEX
// 文字列 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 のメソッドで親子クエリを組めます。

APEX
// 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 のもう一つの大きな価値は、IEloquentMockEloquent に DI で差し替えるだけで、DB を介さない単体テスト が書けることです。

APEX
// 想定するクエリ結果をメモリ上に組み立てる
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 比較

観点生 SOQLApexEloquent
可読性複雑なクエリで急速に悪化✅ メソッドチェーンで構造が見える
動的 WHERE文字列連結を手で書く✅ チェーンメソッドで安全に継ぎ足せる
リレーションリレーション名を覚えて記述withChildren / parentField で直感的
テスト❌ DML + テストデータが必須MockEloquent で DB なしテスト
安全性フィールド名タイポは実行時エラー✅ テスト段階で SELECT 漏れも検知

まとめ

生 SOQL を書き続けることで支払っている代償 (可読性 / 動的条件の面倒さ / 型安全性の欠如 / リレーションの扱いづらさ / テストの重さ) は、別々の問題に見えて、「文字列でクエリを書いている」という選択そのものが根に近い 問題です。

Scribe でクエリを組み立て、IEloquent に実行を委ねるスタイルに切り替えると、これらの代償がまとめて軽くなります。生 SOQL を 1 行も書かない、というほど極端な話ではなく、ユースケースに紐づくクエリを Scribe で構造的に組み立て、保管庫に置く共通クエリも Scribe で部品化していく、というのが現実的な移行像です。

関連ドキュメント