API リファレンス: Scribe
Scribe は ApexEloquent のクエリビルダー。型ヒントとメソッドチェーンで SOQL を組み立てる immutable なクラスです。各メソッドは新しい Scribe インスタンスを返します。
使い方や典型シナリオは Scribe でクエリを組み立てる を参照してください。
Static ファクトリ
| メソッド | 用途 |
|---|---|
Scribe.of(System.Type recordType) | 通常クエリの起点。Scribe.of(Account.class) |
Scribe.source(Schema.SObjectType sObjectType) | 同じく起点。SObjectType を渡す形。Scribe.source(Account.getSObjectType()) |
Scribe.asParent(String parentRelationIdFieldName) | 親リレーション専用 Scribe。parentField / parentCondition / groupByParent に渡す |
Scribe.asChild(System.Type childRecordType) | 子サブクエリ専用 Scribe。withChildren に渡す |
Scribe.asGroup() | WHERE 句を括弧で括る用 Scribe。whereGroup に渡す |
Scribe.asHaving() | HAVING 句専用 Scribe。havingCondition に渡す |
Scribe.asThrough(System.Type junctionType, String relatedKey) | Junction Object 経由の多対多用 Scribe。through に渡す |
SELECT 系
| メソッド | 用途 |
|---|---|
field(String fieldName) | 単一フィールドを SELECT |
fields(List<String> fieldNames) | 複数フィールドを SELECT |
allFields() | 対象 SObject の全フィールドを SELECT |
parentField(Scribe parentScribe) | 親オブジェクトのフィールドを SELECT に組み込む |
withChildren(Scribe childScribe) | 子サブクエリを追加 |
through(Scribe throughScribe) | Junction Object 経由の関連を追加 |
relationName(String relationName) | 子サブクエリ / 多対多 で曖昧性を解消する relationship 名を明示 |
List<String> oppFields = new List<String>{ 'Id', 'Name', 'StageName' };
Scribe scribe = Scribe.of(Opportunity.class)
.fields(oppFields)
.parentField(Scribe.asParent('AccountId').field('Name'));
SELECT id, name, stagename, Account.name FROM Opportunity
SELECT 句の項目名は小文字に正規化され、親項目はリレーション名を前置して並びます (自分の項目 → 親の項目の順)。WHERE 句の項目名は元の表記のままです。
WHERE 系
比較・包含・パターンマッチ
| メソッド | SOQL 出力 |
|---|---|
whereEqual(String f, Object v) | f = v (null → f = NULL) |
whereNotEqual(String f, Object v) | f != v |
whereGreaterThan(String f, Object v) | f > v (null 不可) |
whereGreaterThanOrEqual(String f, Object v) | f >= v |
whereLessThan(String f, Object v) | f < v |
whereLessThanOrEqual(String f, Object v) | f <= v |
whereLike(String f, String pattern) | f LIKE '...' (' は自動エスケープ) |
whereNotLike(String f, String pattern) | f NOT LIKE '...' |
whereIn(String f, Object values) | f IN (...)。List / Set をそのまま受ける (詰め替え不要)。空の扱いは下記参照 |
whereIn(String f, Scribe subQuery) | f IN (SELECT ...) のサブクエリ版 |
whereNotIn(String f, Object values) | f NOT IN (...) |
whereNotIn(String f, Scribe subQuery) | f NOT IN (SELECT ...) |
whereIncludes(String f, List<String> values) | 多値選択リスト INCLUDES |
whereExcludes(String f, List<String> values) | 多値選択リスト EXCLUDES |
whereNull(String f) | f = NULL |
whereNotNull(String f) | f != NULL |
空コレクションの扱い (whereIn / whereNotIn で非対称)
whereIn / whereNotIn に空のコレクションを渡したときの挙動は、両者で異なります。
| メソッド | 空を渡すと | 結果 |
|---|---|---|
whereIn(f, 空) | 常に偽になる条件を組み立てる | 0 件になる |
whereNotIn(f, 空) | 条件ごと無視する | 絞り込みなし = 全件側に倒れる |
SOQL としてはどちらも妥当ですが、「空なら絞り込まない」つもりで whereIn を書くと 0 件になります。意図を明示するなら、次の ignoreWhen を使ってください。
論理結合とグループ化
| メソッド | 用途 |
|---|---|
orCondition() | 次の where を OR で結合 (一度入れたら以降すべて OR) |
whereGroup(Scribe groupScribe) | 条件群を括弧で括る (Scribe.asGroup() から組み立て) |
parentCondition(Scribe parentScribe) | 親オブジェクトの条件で絞り込む |
// (Industry = 'Tech' AND Name = 'X') OR BillingCity = 'Tokyo'
Scribe scribe = Scribe.of(Account.class)
.field('Id')
.whereGroup(
Scribe.asGroup()
.whereEqual('Industry', 'Tech')
.whereEqual('Name', 'X')
)
.orCondition()
.whereEqual('BillingCity', 'Tokyo');
SELECT id FROM Account WHERE (Industry = 'Tech' AND Name = 'X') OR BillingCity = 'Tokyo'
AND と OR を素で混ぜると組み立て時に落ちる (v3.5.0+)
SOQL は 1 つの階層で AND と OR が括弧なしで混ざることを許しません。次はどちらの向きでも ApexEloquentException になり、whereGroup を使うよう案内されます。
// ❌ AND のあとに OR
.whereEqual('Industry', 'Tech').whereEqual('Name', 'X').orCondition().whereEqual('BillingCity', 'Tokyo')
⚠️ v3.5.0 未満では OR-after-AND がすり抜けます。
toSoql()は成功するのに、実行時に SOQL 側がunexpected token: ORで落ちるという分かりにくい失敗になっていました。判定はチェーン時ではなく組み立て時です。
ignoreWhen()による取り下げや、空のwhereNotInのような意味上のスキップで混在が解消されるケースを、正しく通すためです。
条件を動的に取り下げる: ignoreWhen
| メソッド | 用途 |
|---|---|
ignoreWhen(Boolean shouldIgnore) | true のとき、直前の where...() 条件を取り下げる |
「入力が空なら条件を付けない」を、if 分岐や再代入なしに 1 本のチェーンで書けます。
Scribe scribe = Scribe.of(Opportunity.class)
.field('Id')
.whereIn('Id', ids).ignoreWhen(ids.isEmpty())
.whereLike('Name', keyword).ignoreWhen(String.isBlank(keyword));
実行時の値によって、組み上がる SOQL が変わります。
-- ids に値があり、keyword は空のとき
SELECT id FROM Opportunity WHERE Id IN ('006000000000000AAA', '006000000000000AAB')
-- 両方とも空のとき (WHERE 句自体が付かない)
SELECT id FROM Opportunity
上の「空コレクションの扱い」で触れた、空の whereIn が 0 件になる罠も、これで避けられます。
必ず where...() の直後にチェーンします。 先頭で呼ぶ / orderBy() の後で呼ぶ / 2 回続けて呼ぶと ApexEloquentException になります。ignoreWhen() がどの条件に掛かっているかを曖昧にしないための制約です。
whereGroup(...) の直後に置いた場合は、そのグループ全体が取り下げ対象になります。
null 値も取り下げられる (v3.5.0+)
whereGreaterThan / whereGreaterThanOrEqual / whereLessThan / whereLessThanOrEqual / whereLike / whereNotLike / whereIn / whereNotIn / whereIncludes / whereExcludes の 10 種 (オーバーロード込みで 12 メソッド) は null を受け付けません。
v3.5.0 から、この null エラーは組み立て時 (toSoql()) まで遅延します。チェーン時点では「無効な条件」として記録されるだけなので、直後の ignoreWhen(true) で取り下げられます。
// v3.4.x まで: whereGreaterThan の時点で例外 → ignoreWhen に到達しない
// v3.5.0 から: 取り下げられて WHERE 句に出ない
.whereGreaterThan('CloseDate', closeAfter).ignoreWhen(closeAfter == null)
取り下げられずに toSoql() まで残った場合は、どのメソッドのどの項目かを名指しし、ignoreWhen という逃げ道も併記した例外になります。
⚠️
whereEqual/whereNotEqualはこの 12 に含まれません。X = nullは SOQL として正当なので、null がそのまま条件になります。
orCondition との組み合わせ
条件が 1 つも無い状態で orCondition() を呼んでも 何も起きません (no-op)。取り下げによって先頭の条件が消えても落ちず、残ったほうが単独の条件になります。
// 両方あれば OR、片方が消えれば残りが単独条件、両方消えれば WHERE 自体が付かない
Scribe scribe = Scribe.of(Opportunity.class)
.field('Id')
.whereIn('StageName', stages).ignoreWhen(stages.isEmpty())
.orCondition()
.whereIn('OwnerId', ownerIds).ignoreWhen(ownerIds.isEmpty());
-- 両方あるとき
SELECT id FROM Opportunity WHERE StageName IN ('Prospecting') OR OwnerId IN ('005000000000000AAA')
-- stages だけあるとき (OR が消え、単独条件になる)
SELECT id FROM Opportunity WHERE StageName IN ('Prospecting')
-- 両方とも空のとき
SELECT id FROM Opportunity
OR 条件だけが取り下げられた場合、OR モードも一緒に解除されます。取り下げた条件の OR マーカーが残って、後続の素の条件が弾かれる、ということは起きません。
ORDER / LIMIT / OFFSET / forUpdate
| メソッド | 用途 |
|---|---|
orderBy(String field) | ASC 並び替え |
orderBy(String field, String order) | ASC / DESC 指定 |
orderBy(String field, String order, String nullsOperator) | NULLS FIRST / NULLS LAST 指定 |
take(Integer limitNumber) | LIMIT 句 |
offset(Integer offsetNumber) | OFFSET 句 (最大 2000、超えると例外) |
forUpdate() | FOR UPDATE 句 |
制約: forUpdate は orderBy および offset と併用できません。組み立て段階で例外が出ます。
集計関数
| メソッド | SOQL |
|---|---|
count(String field, String alias) | COUNT(field) alias |
countDistinct(String field, String alias) | COUNT_DISTINCT(field) alias |
sum(String field, String alias) | SUM(field) alias |
average(String field, String alias) | AVG(field) alias |
max(String field, String alias) | MAX(field) alias |
min(String field, String alias) | MIN(field) alias |
alias は必須引数 です。Salesforce 標準の AggregateResult はエイリアスを省略するとデフォルトで expr0 / expr1 / ... (宣言順) のフィールド名でアクセスする必要があり、初学者の罠になりがちですが、ApexEloquent はメソッドシグネチャでエイリアスを強制することでこのつまずきを避けています。戻り値の取り出しは aggregateEntry.get('alias 名') で、自分が付けた名前で参照できます。
そのほかの注意:
- 同じ alias を複数の集計関数で使うと例外。
- 子サブクエリ (
withChildren) と集計関数の併用は不可。
GROUP BY / HAVING
| メソッド | 用途 |
|---|---|
groupByField(String fieldName) | 単一フィールドで GROUP BY |
groupByFields(List<String> fieldNames) | 複数フィールドで GROUP BY |
groupByParent(Scribe parentScribe) | 親オブジェクトのフィールドで GROUP BY (Scribe.asParent(...).groupByField(...) を渡す) |
havingCondition(Scribe havingScribe) | HAVING 句 (Scribe.asHaving().whereGreaterThan(alias, value) を渡す) |
Scribe scribe = Scribe.of(OpportunityLineItem.class)
.field('Product2Id')
.sum('TotalPrice', 'totalPrice')
.parentField(
Scribe.asParent('OpportunityId').field('Id').max('Amount', 'maxAmount')
)
.groupByField('Product2Id')
.groupByParent(Scribe.asParent('OpportunityId').groupByField('Id'))
.havingCondition(
Scribe.asHaving().whereGreaterThan('totalPrice', 1000)
);
SELECT product2id, SUM(TotalPrice) totalPrice, Opportunity.id, MAX(Opportunity.Amount) maxAmount
FROM OpportunityLineItem
GROUP BY Product2Id, Opportunity.Id
HAVING SUM(TotalPrice) > 1000
HAVING では alias が集計式に展開されます。whereGreaterThan('totalPrice', 1000) と書けば SUM(TotalPrice) > 1000 になるので、集計式を二度書く必要はありません。
検査・出力
| メソッド | 戻り値 | 用途 |
|---|---|---|
toSoql() | String | 組み立てた SOQL 文字列を返す |
isAggregate() | Boolean | 集計クエリかどうかを判定 (Eloquent が get の振り分けで内部利用) |
buildFieldStructure() | FieldStructure | SELECT 句のフィールド構造を組み立て (MockEntry の SELECT 漏れ検知に内部利用) |
buildAggregateFieldStructure() | FieldStructure | 集計クエリ用のフィールド構造を組み立て |
getSelectedFields(Map<String, SObjectField>) | List<String> | SELECT 対象フィールドのリスト |
toSoql() はデバッグや学習用にも使いますが、実用上の主役はバッチの start() です。
toSoql() でバッチの QueryLocator を組み立てる
Database.getQueryLocator() は SOQL を文字列で要求するため、ここだけは Scribe をそのまま渡せません。組み立ては Scribe で行い、最後に toSoql() で文字列にします。
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(scope().toSoql());
}
Scribe を切り出すと、テストでも SELECT 漏れ検知が効く
バッチには構造上の穴があります。execute(bc, scope) に渡ってくるレコードは プラットフォームが直接渡してくる もので、IEloquent を通りません。つまり「このクエリが何を SELECT したか」という契約が、Usecase 側に届きません。
- 本番:
scopeは実クエリの結果なので、SELECT していない項目に触れればプラットフォームが例外を投げます - テスト:
scopeは自分で組み立てたMockEntryなので、何も検査されません
この差を埋めるのが fetchedBy(scribe) です。クエリの組み立てを @TestVisible なメソッドに切り出しておけば、テストから本番とまったく同じ Scribe を取り出して、モックに焼き付けられます。
public with sharing class RematchCompanyCardsHandler implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(scope().toSoql());
}
// 本番とテストで同じ 1 つの定義を共有する
@TestVisible
private static Scribe scope() {
List<String> cardFields = new List<String>{ 'Id', 'CompanyName__c', 'MatchStatus__c' };
return Scribe.of(BusinessCard__c.class)
.fields(cardFields)
.whereEqual('MatchStatus__c', 'Unprocessed');
}
}
SELECT id, companyname__c, matchstatus__c FROM BusinessCard__c WHERE MatchStatus__c = 'Unprocessed'
// テスト側: 本番の SELECT 契約をモックに焼き付ける
MockEntry card = MockEntry.of(BusinessCard__c.class)
.autoId(1)
.set('CompanyName__c', 'Acme')
.fetchedBy(RematchCompanyCardsHandler.scope());
これで、Usecase が scope() に無い項目を読んだ瞬間に単体テストで落ちます。バッチの start に項目を足し忘れたまま Usecase 側だけ増やした、という食い違いを、本番に出る前に捕まえられます。
fetchedByは API リファレンス: MockEntry を参照してください。
次に読む
- Scribe でクエリを組み立てる: 使い方ガイド
- 親項目・子サブクエリ・多対多: リレーション操作の典型例
- API リファレンス: IEloquent / Eloquent / MockEloquent: 組み立てた Scribe を実行する
- ApexEloquent ガイド: ガイド目次に戻る