# Scribe でクエリを組み立てる

このドキュメントは、ApexEloquent のクエリビルダー `Scribe` を **プロダクションコードでどう書くか** に焦点を当てた使い方ガイドです。全 API のシグネチャ一覧は [API リファレンス: Scribe](/ja/apex-stem/docs/apex-eloquent-api-scribe) を参照してください。他の ApexEloquent トピックの一覧は [ApexEloquent ガイド](/ja/apex-stem/docs/apex-eloquent-guide) から確認できます。

## Scribe とは

`Scribe` は、SOQL を型付きのメソッドチェーンで組み立てるクエリビルダーです。`Scribe.of(Account.class)` から始めて `field`、`whereEqual`、`orderBy` などを積み重ねるだけで、SOQL 文字列が完成します。

```apex
Scribe accountScribe = Scribe.of(Account.class)
  .field('Id')
  .field('Name')
  .field('Industry')
  .whereEqual('Industry', 'Technology')
  .orderBy('Name', 'ASC')
  .take(10);
// → SELECT id, name, industry FROM Account WHERE Industry = 'Technology' ORDER BY Name ASC LIMIT 10

List<IEntry> accounts = new Eloquent().get(accountScribe);
```

ポイントは 3 つです。

- **不変 (immutable)**: 各メソッドは新しい `Scribe` インスタンスを返します。途中で分岐させて条件違いのクエリを派生させても、元の `Scribe` は変わりません。
- **クエリ実行は `IEloquent` に委譲**: 完成した `Scribe` を `IEloquent.get(scribe)` に渡すと、内部で SOQL が発行されてデータが取得されます。この **クエリ組み立てと実行の分離** が ApexEloquent の中核で、詳しくは [Query Delegation Pattern](/ja/apex-stem/docs/query-delegation-pattern) で解説しています。
- **DI で Mock に差し替え可能**: `IEloquent` はインターフェースで、本番では `Eloquent` (実 SOQL を発行)、テストでは `MockEloquent` (DB を介さず、注入された `IEntry` をそのまま返す) を Layered Constructor Pattern で差し替えられます。Usecase 側のコードは `IEloquent` 型のフィールドに依存するので、本番もテストも呼び方は同じまま、テスト時だけ DB を介さずロジックを検証できます。詳しくは [データ取得と DML、IEntry、Mock](/ja/apex-stem/docs/apex-eloquent-data-access) を参照してください。

フィールド名は `SObjectField` ではなく **文字列** で渡します (`'Id'` / `'Industry__c'` など)。これにより、ビルダーは Apex の型システムに縛られず、実行時に動的に組み立てられます。

## SELECT する項目を決める

### 単一フィールドと複数フィールド

`field(String fieldName)` で 1 つずつ追加するか、`fields(List<String>)` でまとめて渡します。

```apex
Scribe scribe = Scribe.of(Opportunity.class)
  .field('Id')
  .field('Name')
  .field('StageName');
// → SELECT id, name, stagename FROM Opportunity
```

`fields(List<String>)` には、あらかじめ宣言した `List<String>` をそのまま渡すこともできます。

```apex
List<String> opportunityFields = new List<String>{
  'Id',
  'Name',
  'StageName',
  'CloseDate',
  'Amount'
};
Scribe scribe = Scribe.of(Opportunity.class)
  .fields(opportunityFields)
  .whereEqual('StageName', 'Prospecting');
// → SELECT id, name, stagename, closedate, amount FROM Opportunity WHERE StageName = 'Prospecting'
```

### parentField で親項目を取り込む

`Scribe.asParent('AccountId').field(...)` を `parentField` に渡すと、親オブジェクトのフィールドが SELECT 句に取り込まれます。

```apex
Scribe scribe = Scribe.of(Opportunity.class)
  .field('Id')
  .field('Name')
  .parentField(Scribe.asParent('AccountId').field('Name').field('Industry'))
  .whereEqual('StageName', 'Closed Won');
// → SELECT id, name, Account.name, Account.industry FROM Opportunity WHERE StageName = 'Closed Won'
```

子サブクエリ (`withChildren`) や多対多 (`through`) は [親項目・子サブクエリ・多対多](/ja/apex-stem/docs/apex-eloquent-relations) で詳しく扱います。

### allFields で全件 SELECT

`allFields()` を使うと、対象オブジェクトのアクセス可能な全フィールドを SELECT します。プロダクションでは必要なフィールドを明示するほうが安全ですが、テストデータの構築や調査用には便利です。

## WHERE で絞り込む

### 基本の WHERE

等価 (`whereEqual` / `whereNotEqual`)、比較 (`whereGreaterThan` 系 / `whereLessThan` 系)、パターン (`whereLike` / `whereNotLike`)、リスト (`whereIn` / `whereNotIn`)、多値選択 (`whereIncludes` / `whereExcludes`)、null チェック (`whereNull` / `whereNotNull`) があります。シグネチャと挙動の詳細は [API リファレンス: Scribe](/ja/apex-stem/docs/apex-eloquent-api-scribe) を参照。

```apex
Scribe scribe = Scribe.of(Opportunity.class)
  .field('Id')
  .whereEqual('StageName', 'Prospecting')
  .whereGreaterThan('Amount', 1000)
  .whereIn('OwnerId', ownerIds);  // Set<Id> をそのまま渡せる
// → SELECT id FROM Opportunity WHERE StageName = 'Prospecting' AND Amount > 1000 AND OwnerId IN (...)
```

連続した `whereXxx` はデフォルトで **AND** で結合されます。

### AND と OR を混ぜる

OR を入れたい場合は `orCondition()` を **次の where の前に** 挟みます。

```apex
Scribe scribe = Scribe.of(Account.class)
  .field('Id')
  .whereEqual('Name', 'A')
  .orCondition()
  .whereEqual('Name', 'B')
  .orCondition()
  .whereEqual('Name', 'C');
// → SELECT id FROM Account WHERE Name = 'A' OR Name = 'B' OR Name = 'C'
```

**OR を一度入れたら、以降の where はすべて OR で結合する必要があります**。途中で AND に戻すことはできません。OR と AND を混ぜたい時は、**AND 条件を前に集めて、OR 条件を後ろに置く** か、`whereGroup` + `Scribe.asGroup()` で片側を括弧でまとめます。

```apex
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'
```

なお、`whereIn` に **空のリスト** を渡すと、SOQL では `Id = null` (= 必ず偽の条件) に変換されます。「絞り込み対象が 0 件のときは結果も 0 件」という挙動を、呼び出し側で `isEmpty()` チェックせずにそのまま書ける仕様です。

⚠️ `whereNotIn` は逆で、**空を渡すと条件ごと無視**されます (絞り込みなし = 全件側)。「空なら絞り込まない」つもりで `whereIn` を書くと 0 件になるので、意図する側を明示したいときは後述の `ignoreWhen` を使ってください。

### サブクエリで IN する

`whereIn` の第 2 引数には **別の `Scribe`** を渡せます。「先に ID を取ってきてから次のクエリで使う」という二段 SOQL を 1 本にまとめられます。

```apex
// 「フォローしている取引先」に紐づく商談だけを取る
Scribe followedAccountIds = Scribe.of(AccountShare.class)
  .field('AccountId')
  .whereEqual('UserOrGroupId', UserInfo.getUserId());

Scribe oppScribe = Scribe.of(Opportunity.class)
  .field('Id')
  .field('Name')
  .whereIn('AccountId', followedAccountIds);
// → SELECT id, name FROM Opportunity WHERE AccountId IN (SELECT accountid FROM AccountShare WHERE UserOrGroupId = '...')
```

SOQL の `IN (SELECT ...)` 構文がそのまま使えるイメージです。`whereNotIn` でも同じく `Scribe` を渡せます。

### whereLike は SQL インジェクションを自動エスケープ

`whereLike(field, pattern)` に渡した `pattern` 内のシングルクォート (`'`) は自動でエスケープされます。ユーザー入力をそのまま渡しても SOQL インジェクションは起きません。

```apex
String userInput = "O'Brien";  // 一見危なそうな入力
Scribe scribe = Scribe.of(Contact.class)
  .field('Id')
  .whereLike('LastName', '%' + userInput + '%');
// → ...WHERE LastName LIKE '%O\'Brien%'
```

## 並び替え・LIMIT・FOR UPDATE

`orderBy` / `take` / `offset` / `forUpdate` で並び替え・件数制限・ロックが書けます。

```apex
Scribe scribe = Scribe.of(Opportunity.class)
  .field('Id')
  .field('CloseDate')
  .whereEqual('StageName', 'Prospecting')
  .orderBy('CloseDate', 'DESC')
  .take(20);
// → SELECT id, closedate FROM Opportunity WHERE StageName = 'Prospecting' ORDER BY CloseDate DESC LIMIT 20
```

`forUpdate` を `orderBy` / `offset` と併用すると例外、`offset` は最大 2000 など、SOQL の制約をそのまま反映する組み立て時チェックがあります。詳細は [API リファレンス: Scribe](/ja/apex-stem/docs/apex-eloquent-api-scribe) を参照。

## 集計クエリ (Aggregate)

「親 ID ごとに子レコードの件数を集計」のようなケースでは、Apex 側の SOQL 行数とヒープ消費を抑えるため、集計クエリを使います。

```apex
Scribe eventScribe = Scribe.of(Event.class)
  .field('WhatId')               // GROUP BY する field は SELECT にも明示
  .count('Id', 'eventCount')     // alias 付きで COUNT
  .whereIn('WhatId', opportunityIds)
  .groupByField('WhatId');
// → SELECT whatid, COUNT(Id) eventCount FROM Event WHERE WhatId IN (...) GROUP BY WhatId

List<IEntry> aggregateEntries = new Eloquent().get(eventScribe);

for (IEntry aggregateEntry : aggregateEntries) {
  Id whatId = (Id) aggregateEntry.get('WhatId');
  Integer cnt = ((Decimal) aggregateEntry.get('eventCount')).intValue();
}
```

集計関数は `count` / `countDistinct` / `sum` / `average` / `max` / `min` の 6 種類。GROUP BY 系は `groupByField` / `groupByFields` / `groupByParent`、HAVING 句は `havingCondition(Scribe.asHaving()...)` で組み立てます。それぞれのシグネチャは [API リファレンス: Scribe](/ja/apex-stem/docs/apex-eloquent-api-scribe) を参照。

**エイリアスの付与は必須** です。各集計関数は `count(field, alias)` のように `alias` を省略できない 2 引数で、戻り値は `aggregateEntry.get('alias 名')` で取り出します。Salesforce 標準の `AggregateResult` はデフォルトで `expr0` / `expr1` / ... のフィールド名 (宣言順) でアクセスする必要があり初学者の罠になりがちですが、ApexEloquent はメソッドシグネチャでエイリアスを強制することでこのつまずきを避けています。

### 親フィールドの集計とグループ化

「親オブジェクトのフィールド」を集計や GROUP BY の対象にしたい場合、**直接 `field('Account.Industry')` のような書き方はできません**。親に関するものは `Scribe.asParent(...)` を経由する必要があります。

例: 商品 (`Product2`) ごとの注文明細合計と、その明細が属する商談 (`Opportunity`) の最高額を、商品 × 商談単位で集計する。

```apex
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')
  );
// → SELECT product2id, SUM(TotalPrice) totalPrice, Opportunity.id, MAX(Opportunity.Amount) maxAmount FROM OpportunityLineItem GROUP BY Product2Id, Opportunity.Id
```

ポイント:
- 親の SELECT 項目と集計関数は `parentField(Scribe.asParent('OpportunityId').field(...).max(...))` の中にまとめる
- 親フィールドでの GROUP BY は `groupByParent(Scribe.asParent('OpportunityId').groupByField('Id'))` で表現する
- HAVING 句で親由来 alias (`maxAmount`) を参照するときも、`Scribe.asHaving().whereGreaterThan('maxAmount', 1000)` のように同じ alias で書ける

### 集計クエリの注意点

- **GROUP BY する field は SELECT にも明示する** (`field('Product2Id')` を忘れない)
- **集計結果は `Decimal` 型** で返る。`Integer` に入れる時は `((Decimal) aggregateEntry.get('alias')).intValue()` でキャスト
- 取得 API は通常の `get(scribe)` のままで OK。**集計クエリか通常クエリかはフレームワークが自動判別** します
- 同じ alias を複数の集計関数で使うと例外になります (`sum('A','total')` と `max('B','total')` のように `total` を重複させない)
- 子サブクエリ (`withChildren`) と集計関数の併用は SOQL の制約により不可

## 動的にクエリを組み立てる

検索フィルタのように「入力があるときだけ条件を付ける」クエリは、`ignoreWhen` で**分岐なしの 1 本のチェーン**として書けます。

```apex
Scribe scribe = Scribe.of(Opportunity.class)
  .field('Id')
  .field('Name')
  .whereEqual('Industry', industry).ignoreWhen(industry == null)
  .whereIn('StageName', stages).ignoreWhen(stages.isEmpty())
  .whereGreaterThan('CloseDate', closeAfter).ignoreWhen(closeAfter == null);

List<IEntry> opps = this.fetchEloquent.get(scribe);
// 3 つ全部が渡された時:
// → SELECT id, name FROM Opportunity WHERE Industry = 'Technology' AND StageName IN (...) AND CloseDate > 2026-01-01
// 何も渡されなかった時:
// → SELECT id, name FROM Opportunity
```

`ignoreWhen(true)` は**直前の条件を取り下げます**。空の `whereIn` が `0 件` になってしまう罠も、これで避けられます (詳細は [API リファレンス: Scribe](/ja/apex-stem/docs/apex-eloquent-api-scribe))。

> ⚠️ **値が `null` のケースは v3.5.0 以上が必要です。** `whereGreaterThan` / `whereIn` など **null を受け付けない 12 メソッド**は、v3.4.x までチェーンした瞬間に例外を投げていたため、後続の `ignoreWhen` に到達できませんでした。v3.5.0 でエラーが**組み立て時まで遅延**するようになり、`ignoreWhen` で取り下げられます。上の `closeAfter == null` の行はまさにこのケースです。

### if で積み上げる書き方

`ignoreWhen` が無いバージョンでは、条件分岐で積み上げます。`Scribe` は不変なので **再代入** (`scribe = scribe.whereXxx(...)`) が必要です。

```apex
Scribe scribe = Scribe.of(Opportunity.class)
  .field('Id')
  .field('Name');

if (industry != null) {
  scribe = scribe.whereEqual('Industry', industry);
}
if (stages != null && !stages.isEmpty()) {
  scribe = scribe.whereIn('StageName', stages);
}
```

この形は今も動きますが、条件が増えるほど「クエリの形」が分岐の中に散らばって読みにくくなります。新しく書くなら `ignoreWhen` に寄せてください。

### 派生クエリを作る

各メソッドが **新しい `Scribe` インスタンス** を返すので、1 本のクエリから派生を作れます。元の `Scribe` は変わりません。たとえば「同じ商談集合に対して、件数取得用と一覧取得用の 2 本を組む」ようなパターンです。

```apex
Scribe baseScribe = Scribe.of(Opportunity.class)
  .whereEqual('StageName', 'Prospecting')
  .whereGreaterThan('Amount', 1000);

// 件数だけ取りたい
Scribe countScribe = baseScribe.count('Id', 'cnt');
// → SELECT COUNT(Id) cnt FROM Opportunity WHERE StageName = 'Prospecting' AND Amount > 1000

// 一覧を取りたい
Scribe listScribe = baseScribe
  .field('Id')
  .field('Name')
  .orderBy('CloseDate', 'ASC')
  .take(50);
// → SELECT id, name FROM Opportunity WHERE StageName = 'Prospecting' AND Amount > 1000 ORDER BY CloseDate ASC LIMIT 50
```

`baseScribe` は両方の派生元として変わらず再利用できます。

## 次に読む

- [データ取得と DML、IEntry、Mock](/ja/apex-stem/docs/apex-eloquent-data-access): 組み立てた `Scribe` を `IEloquent` で実行し、`IEntry` を扱う
- [親項目・子サブクエリ・多対多](/ja/apex-stem/docs/apex-eloquent-relations): リレーションを使ったクエリと、`MockEntry` での親子モック
- [ApexEloquent ガイド](/ja/apex-stem/docs/apex-eloquent-guide): ApexEloquent ガイドの目次に戻る
