# API リファレンス: IEloquent / Eloquent / MockEloquent

`IEloquent` は ApexEloquent のデータアクセス契約 (インターフェース)。本番では `Eloquent`、テストでは `MockEloquent` を Layered Constructor Pattern で DI して使います。

使い方や典型シナリオは [データ取得と DML、IEntry、Mock](/ja/apex-stem/docs/apex-eloquent-data-access) を参照してください。

> このページは v3 系・v2 系の共通内容です。両者の違いは実行モードだけなので、そこだけ「実行モード (v3 系)」として分けて示します。

## 3 つの関係

```
IEloquent  (interface)       ← Usecase が依存する契約
   ↑                       
   ├── Eloquent             ← 本番。標準 SOQL / DML をそのまま叩く
   └── MockEloquent         ← テスト。DB を介さず、Spy + failOn* を提供
```

Usecase は `IEloquent` 型のフィールドに本番では `new Eloquent()`、テストでは `new MockEloquent(...)` を Layered Constructor で受け取ります。

## IEloquent (インターフェース)

すべての契約メソッドは `Eloquent` と `MockEloquent` の両方で実装されています。

### ラベリング系

| シグネチャ | 戻り値 | 用途 |
|---|---|---|
| `label(String labelName)` | `IEloquent` | 直後の 1 操作にラベルを付与してチェーン可能にする。1 つの `IEloquent` を「先月用」「今期用」「Account 更新用」のように **ラベルで多重化** できるので、用途ごとに `IEloquent` を分けて DI する代わりに 1 本にまとめられる |

```apex
// 同じ IEloquent で 2 つの SOQL と 1 つの DML を区別する
IEntry job = this.eloquent.label('jobLoad').firstOrFail(jobScribe);
List<IEntry> details = this.eloquent.label('detailLoad').get(detailScribe);
this.eloquent.label('finalDml').doUpdate(toUpdate);
```

`null` / 空文字を `label(...)` に渡すと例外になります。

`'default'` は **ラベルなしの操作に内部で割り当てられる名前**です。`label('default')` 自体は通りますが、ラベルなしの呼び出しと同じ枠を指すことになるので、意図が伝わりません。別の名前を付けてください (`whenLabel('default')` のほうは冗長として明示的に弾かれます)。

**opt-in strict mode**: `IEloquent` インスタンスで初めて `.label(...)` を呼んだ瞬間、そのインスタンスは **strict mode** にスイッチします (インスタンスの生存中ずっと sticky)。

- 以降のすべての操作は `.label(...)` を前置する必要がある (ラベルなし操作は例外)
- 同じラベルを同じインスタンスで 2 回使うと例外
- 一度も `.label()` を呼ばないインスタンスは従来通り (= lenient mode、後方互換)

これは「新しい DML を足したのにラベルを忘れていた」という事故を実行時に塞ぐためのガード。silent に default バケットに溜まる footgun を防ぎます。

### 実行モード系 (v3 系のみ)

| シグネチャ | 戻り値 | 用途 |
|---|---|---|
| `userMode()` | `IEloquent` | 以降の SOQL / DML を `AccessLevel.USER_MODE` で実行する (v3 系の既定) |
| `systemMode()` | `IEloquent` | 以降の SOQL / DML を `AccessLevel.SYSTEM_MODE` で実行する。FLS・オブジェクト権限を無視する |

v3 系では `Eloquent` が `inherited sharing` になり、SOQL / DML の既定が **ユーザーモード** (実行ユーザーの項目・オブジェクト権限を尊重) に変わりました。集計・焼き付け・データ移行のように「誰が起こしても完遂すべき処理」だけ `systemMode()` で明示的にオプトアウトします。

```apex
// system プロセス: 呼び出し元クラスを without sharing にしたうえで明示する
this.eloquent.systemMode().label(LBL_UPDATE).doUpdate(entries);
```

- **sticky**: 一度呼ぶと、そのインスタンスの以降すべての操作に効きます (`label()` のように操作ごとにリセットされません)
- **`MockEloquent` では no-op**: 自身をそのまま返すだけなので、単体テストはモードを意識せずに書けます
- ⚠️ `systemMode()` が外すのは **FLS・オブジェクト権限だけ** です。**共有 (レコードの可視性) は別軸**で、外すには呼び出し元クラスを `without sharing` にする必要があります

### クエリ系

| シグネチャ | 戻り値 | 用途 |
|---|---|---|
| `get(Scribe scribe)` | `List<IEntry>` | クエリ実行。0 件なら空リスト |
| `first(Scribe scribe)` | `IEntry` | 先頭 1 件。0 件なら `null` |
| `firstOrFail(Scribe scribe)` | `IEntry` | 先頭 1 件。0 件なら `ApexEloquentException` |
| `firstOrFail(Scribe scribe, Exception orFail)` | `IEntry` | 先頭 1 件。0 件なら **渡した例外をそのまま** throw する |
| `getAsSObject(Scribe scribe)` | `List<SObject>` | `SObject` リスト (最終手段) |
| `firstAsSObject(Scribe scribe)` | `SObject` | 先頭 1 件の SObject 版 |
| `firstOrFailAsSObject(Scribe scribe)` | `SObject` | 先頭 1 件の SObject 版、0 件で例外 |
| `rawSoql(String soql)` | `List<IEntry>` | `Scribe` をバイパスして生 SOQL を実行 (最終手段。SELECT 漏れ検知が無効化される) |

**`firstOrFail(scribe, orFail)` の用途**: 0 件を業務エラーとして画面に返したいときに使います。渡した例外がそのまま throw されるので、`first` + null 判定 + 自前 throw を書かずに済みます。

効いてくるのは **catch した先**です。業務例外を渡しておけば、そのまま `catch (UsecaseException)` で受けられます。

```apex
IEntry job;
try {
  job = this.eloquent.label(LBL_JOB).firstOrFail(
    jobScribe,
    new UsecaseException('対象のジョブが見つかりませんでした。')
  );
} catch (UsecaseException ex) {
  this.t.skip('業務エラー: ' + ex.getMessage());
}
```

引数なしの `firstOrFail(scribe)` だと `ApexEloquentException` が飛ぶので、この `catch` には入りません。

### DML 系

| シグネチャ | 戻り値 |
|---|---|
| `doInsert(SObject record)` | `SObject` |
| `doInsert(List<SObject> records)` | `List<SObject>` |
| `doUpdate(SObject record)` | `SObject` |
| `doUpdate(IEntry entry)` | `IEntry` |
| `doUpdate(List<SObject> records)` | `List<SObject>` |
| `doUpdate(List<IEntry> entries)` | `List<IEntry>` |
| `doUpdate(SObject record, Boolean allOrNone)` | `Database.SaveResult` |
| `doUpdate(IEntry entry, Boolean allOrNone)` | `Database.SaveResult` |
| `doUpdate(List<SObject> records, Boolean allOrNone)` | `List<Database.SaveResult>` |
| `doUpdate(List<IEntry> entries, Boolean allOrNone)` | `List<Database.SaveResult>` |
| `doUpsert(SObject record)` | `SObject` |
| `doUpsert(IEntry entry)` | `IEntry` |
| `doUpsert(List<SObject> records)` | `List<SObject>` |
| `doUpsert(List<IEntry> entries)` | `List<IEntry>` |
| `doUpsertByExternalId(SObject record, Schema.SObjectField externalIdField, Boolean allOrNone)` | `Database.UpsertResult` |
| `doUpsertByExternalId(IEntry entry, Schema.SObjectField externalIdField, Boolean allOrNone)` | `Database.UpsertResult` |
| `doUpsertByExternalId(List<SObject> records, Schema.SObjectField externalIdField, Boolean allOrNone)` | `List<Database.UpsertResult>` |
| `doUpsertByExternalId(List<IEntry> entries, Schema.SObjectField externalIdField, Boolean allOrNone)` | `List<Database.UpsertResult>` |
| `doDelete(SObject record)` | `void` |
| `doDelete(IEntry entry)` | `void` |
| `doDelete(List<SObject> records)` | `void` |
| `doDelete(List<IEntry> entries)` | `void` |

bulk 化を基本とし、単件版は処理対象が常に 1 件のときだけ使います。

**`allOrNone` 引数付き `doUpdate` の用途**: 戻り値が `Database.SaveResult` / `List<Database.SaveResult>` なので、partial-success 時の per-record エラー集約をユニットテストで検証できます。ETL / 増分同期 / バッチマイグレーションで「失敗したレコードだけログに残して続行する」ようなパスがある時に使います。

**`doUpsertByExternalId` の用途**: 標準 `Database.upsert(records, externalIdField, allOrNone)` の API を `IEloquent` 抽象の下に取り込んだもの。ETL / 増分同期 / バッチマイグレーションで必須となる「外部 ID キーによる upsert」を、本番 / Mock 共通の契約で扱えます。

## Eloquent (Production)

`IEloquent` を実装する本番クラス。標準の SOQL / DML をそのまま叩きます。**追加の public メソッドは無し** (インターフェース通り)。

```apex
IEloquent eloquent = new Eloquent();
List<IEntry> opps = eloquent.get(scribe);
eloquent.doUpdate(opps);
```

## MockEloquent (Mock 拡張)

`IEloquent` の本契約に加えて、テスト用の **Spy プロパティ** と **failOn シリーズ** を持ちます。

### コンストラクタ

| シグネチャ | 振る舞い |
|---|---|
| `new MockEloquent()` | 空。ラベルを使う場合は `attach(...)` でデータを与える |
| `new MockEloquent(IEntry entry)` | ラベルなし (`'default'`) の返却データとして 1 件をプリロード |
| `new MockEloquent(List<IEntry> entries)` | 同上、リスト版 |

コンストラクタで渡したデータが供給されるのは **ラベルなし (`'default'`) の操作だけ**です。本番コードが `label(...)` を使っている場合は、コンストラクタではなく `attach(label, ...)` で与えます。

`MockEloquent` は **`Scribe` の WHERE 条件を評価せず**、渡したリストをそのまま返します。条件違いのクエリを区別したい場合は、**クエリ単位にラベルを付けて `attach(label, ...)` で仕分けます** (下記)。

### 未 attach のラベルは例外になる

`label('X')` で `get` / `first` / `firstOrFail` を呼んだのに `attach('X', ...)` していない場合、**テスト実行時は例外**になります。エラーには attach 済みのラベル一覧が付くので、打ち間違いはその場で分かります。

これは「ラベル名を間違えた → 0 件が返る → 対象なしでスキップの分岐に入る → **何も検証していないのにテストが緑**」という偽陽性を塞ぐためのガードです。

「0 件の経路」を意図してテストしたいときは、**空リストを明示的に attach** して意図を宣言します。

```apex
MockEloquent mock = (new MockEloquent())
  .attach(MyUsecase.LBL_FETCH, new List<IEntry>());
```

> 古いバージョンから上げると、この変更で赤くなるテストが出ることがあります。それは「attach 漏れで何も検証せずに緑だった」テストです。機械的に空 attach を足して緑に戻すのではなく、本来そこに注入すべきだったデータを確認してください。

### Spy プロパティとラベル別アクセサ

| プロパティ / メソッド | 型 | 中身 |
|---|---|---|
| `upsertedRecords` | `List<SObject>` | `doInsert` / `doUpdate` / `doUpsert` / `doUpsertByExternalId` で渡されたレコードのうち **`'default'` バケット** の累積 (※ `@deprecated`: 新規コードは `upsertedRecordsAt('default')` を推奨) |
| `deletedCount` | `Integer` | `doDelete` の呼び出し件数のうち **`'default'` バケット** の累計 (※ `@deprecated`: 新規コードは `deletedCountAt('default')` を推奨) |
| `upsertedRecordsAt(String label)` | `List<SObject>` | 指定ラベルの DML レコード累積 |
| `deletedCountAt(String label)` | `Integer` | 指定ラベルの delete 件数累計 |
| `attach(String label, IEntry entry)` | `MockEloquent` | 指定ラベルのクエリ返却データとして 1 件をプリロード (チェーン可能、同じラベルへの再 attach は上書き) |
| `attach(String label, List<IEntry> entries)` | `MockEloquent` | 同上、リスト版 |
| `failSave(Id recordId, String errorMessage)` | `MockEloquent` | 指定 Id のレコードだけ **保存失敗** させる (チェーン可能) |

### failSave() で部分失敗を作る

`allOrNone` 付きの DML (`doUpdate(records, false)` / `doUpsertByExternalId(..., false)`) の `SaveResult` / `UpsertResult` を、**レコード単位で失敗**にできます。対象は Id で指名するので、`MockEntry.autoId()` と対で使います。

```apex
Id badId = MockEntry.of(Account.class).autoId(2).getId();

MockEloquent mock = (new MockEloquent())
  .failSave(badId, '入力規則で拒否されました');
```

- `allOrNone = false` — そのレコードの結果が `success = false` と指定メッセージを持ち、**Spy には積まれません** (保存されていないため)
- `allOrNone = true` — 実際の all-or-nothing DML と同じく、**何も記録される前に操作全体が例外**になります

「失敗したレコードだけログに残して続行する」パスを、実 DML なしで検証できます。

```apex
// ラベルなしの従来の使い方 (後方互換)
MockEloquent updateEloquent = new MockEloquent();
(new MyUsecase(input, fetchEloquent, updateEloquent)).invoke();
Assert.areEqual(1, updateEloquent.upsertedRecords.size());
Opportunity updated = (Opportunity) updateEloquent.upsertedRecords[0];
Assert.areEqual('Technology', updated.Industry__c);
```

```apex
// ラベル多重化版 — 1 つの MockEloquent でクエリ返却と DML 検証をラベルで仕分け
MockEloquent mock = (new MockEloquent())
  .attach('jobLoad', jobEntry);

(new FinalizeJobUsecase(jobId, mock)).invoke();

Assert.areEqual(1, mock.upsertedRecordsAt('finalDml').size());
```

**後方互換性ノート**: 既存の `upsertedRecords` / `deletedCount` パブリックフィールドは引き続き動作し、内部的に `'default'` バケットを反映します。JSDoc 上で `@deprecated` 化されており、段階的に `*At('default')` への移行が推奨されます。また、レガシーコンストラクタ `new MockEloquent(List<IEntry>)` も引き続き動作し、渡されたデータは `'default'` ソースに供給されます。

### failOn シリーズ (例外シミュレーション)

各メソッドは引数なし版と `Exception` 受け取り版の 2 オーバーロード。`Exception` を渡さない場合はデフォルト例外が投げられます。

| メソッド | 対応する契約 |
|---|---|
| `failOnGet()` / `failOnGet(Exception e)` | `get(scribe)` |
| `failOnFirst()` / `failOnFirst(Exception e)` | `first(scribe)` |
| `failOnFirstOrFail()` / `failOnFirstOrFail(Exception e)` | `firstOrFail(scribe)` |
| `failOnGetAsSObject()` / `failOnGetAsSObject(Exception e)` | `getAsSObject(scribe)` |
| `failOnFirstAsSObject()` / `failOnFirstAsSObject(Exception e)` | `firstAsSObject(scribe)` |
| `failOnFirstOrFailAsSObject()` / `failOnFirstOrFailAsSObject(Exception e)` | `firstOrFailAsSObject(scribe)` |
| `failOnRawSoql()` / `failOnRawSoql(Exception e)` | `rawSoql(soql)` |
| `failOnDoInsert()` / `failOnDoInsert(Exception e)` | `doInsert(*)` |
| `failOnDoUpdate()` / `failOnDoUpdate(Exception e)` | `doUpdate(*)` |
| `failOnDoUpsert()` / `failOnDoUpsert(Exception e)` | `doUpsert(*)` |
| `failOnDoUpsertByExternalId()` / `failOnDoUpsertByExternalId(Exception e)` | `doUpsertByExternalId(*)` |
| `failOnDoDelete()` / `failOnDoDelete(Exception e)` | `doDelete(*)` |

### whenLabel() で失敗をラベルにスコープする

| シグネチャ | 用途 |
|---|---|
| `whenLabel(String label)` | 直前の `failOn*()` を **特定のラベルでのみ発火** するようスコープする (チェーン可能) |

```apex
// label('finalDml') の DML だけが失敗する、 他のラベルは正常に動く
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('Simulated finalDml failure'))
  .whenLabel('finalDml');
```

「fail on get when label is X」のように、ラベル多重化 Usecase で「この副作用だけ落としたい」ケースを表現できます。

```apex
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('Simulated DML failure'))
  .whenLabel(MyUsecase.LBL_UPDATE);

try {
  (new MyUsecase(input, mock)).invoke();
  Assert.fail('Expected exception');
} catch (DmlException e) {
  Assert.isTrue(TraceFlow.isLastAbort());
}
```

### repeat() で「常に失敗」を表現

| シグネチャ | 用途 |
|---|---|
| `repeat()` | 直前の `failOn*` を **以降ずっと繰り返し** たい時に呼ぶ (回数指定はできない) |

`failOn*()` 単独だと「次の 1 回だけ失敗」です。`failOn*` は **同じメソッドに対してキューに積まれる** ので、2 回並べれば 1 回目と 2 回目が失敗し 3 回目は成功します (リトライの成功パス)。

`.repeat()` を付けると、直前に仕掛けた設定が **以降すべての呼び出しで失敗** し続けます。「最大 3 回までリトライ、それでもダメなら中止」というロジックの **中止側のパス** を、回数を数えずに確かめられます。

キューは **ラベルごとに独立** しています。`whenLabel` を付けなかった設定は、ラベルなしの呼び出し (`'default'`) 宛てになります。

⚠️ テスト設定のビルダー (`attach` / `failOn*` / `whenLabel` / `failSave` / `repeat`) は、いずれも **`this` を変更せず新しいインスタンスを返します**。戻り値を受け取らないと設定が消えるので、チェーンでつなぐか受け直してください。

💡 ラベルの「1 度だけ」制約が数えるのは **成功した操作** です。失敗した操作はラベルを解放するため、本番コードが catch して同じラベルでリトライする実装もそのままテストできます。

⚠️ ラベルを使っているテストで `whenLabel` を付け忘れると、その設定は `'default'` 宛てになり永久に発火しません。これは診断メッセージ付きの例外として検出されます (付けるべきラベル名も表示されます)。

### 表現できる失敗パターン

`failOn*` / `whenLabel` / `repeat` の組み合わせで書ける形は、次の 5 つに整理できます。

| 書き方 | 起きること |
|---|---|
| `failOnGet(exA).failOnGet(exB)` | 1 回目は `exA`、2 回目は `exB` で失敗し、**3 回目は成功** |
| `failOnGet(ex).repeat()` | **毎回失敗** |
| `failOnGet(ex).whenLabel('opp')` | `'opp'` の呼び出しが 1 回失敗し、**リトライすれば成功** |
| `failOnGet(exA).whenLabel('opp')`<br>`.failOnGet(exB).whenLabel('opp')` | `'opp'` が 2 回連続で失敗し、**3 回目は成功** |
| `failOnGet(ex).whenLabel('opp').repeat()` | `'opp'` の呼び出しが**毎回失敗** |

```apex
// 「1 回失敗 → リトライで成功」を書く
MockEloquent mock = (new MockEloquent())
  .attach('opp', new List<IEntry>{ oppEntry })
  .failOnGet(new QueryException('boom'))
  .whenLabel('opp');

// 1 回目: 例外
// 2 回目: 失敗がラベルを解放しているので、attach したデータが返る
```

キューは**ラベルごとに独立**しているので、同じメソッドに対してラベル違いの失敗を並べても、互いに干渉しません。

```apex
// 取得は 'fetch' で、保存は 'update' で、それぞれ別の理由で落とす
MockEloquent mock = (new MockEloquent())
  .failOnGet(new QueryException('取得に失敗')).whenLabel('fetch')
  .failOnDoUpdate(new DmlException('保存に失敗')).whenLabel('update');
```

これらに `failSave` (呼び出しは成功するが特定レコードだけ保存されない) を加えたものが、`MockEloquent` で表現できる失敗の全体です。

## 次に読む

- [データ取得と DML、IEntry、Mock](/ja/apex-stem/docs/apex-eloquent-data-access): 使い方ガイド
- [API リファレンス: Scribe](/ja/apex-stem/docs/apex-eloquent-api-scribe): クエリ組み立て側
- [API リファレンス: IEntry / Entry / MockEntry](/ja/apex-stem/docs/apex-eloquent-api-entry): 返り値の `IEntry` 側
- [Layered Constructor Pattern](/ja/apex-stem/docs/layered-constructor-pattern): `IEloquent` を Usecase に DI する設計
- [ApexEloquent ガイド](/ja/apex-stem/docs/apex-eloquent-guide): ガイド目次に戻る
