API リファレンス: IEloquent / Eloquent / MockEloquent
IEloquent は ApexEloquent のデータアクセス契約 (インターフェース)。本番では Eloquent、テストでは MockEloquent を Layered Constructor Pattern で DI して使います。
使い方や典型シナリオは データ取得と DML、IEntry、Mock を参照してください。
このページは 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 本にまとめられる |
// 同じ 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() で明示的にオプトアウトします。
// 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) で受けられます。
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 メソッドは無し (インターフェース通り)。
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 して意図を宣言します。
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() と対で使います。
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 なしで検証できます。
// ラベルなしの従来の使い方 (後方互換)
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);
// ラベル多重化版 — 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*() を 特定のラベルでのみ発火 するようスコープする (チェーン可能) |
// label('finalDml') の DML だけが失敗する、 他のラベルは正常に動く
MockEloquent mock = (new MockEloquent())
.failOnDoUpdate(new DmlException('Simulated finalDml failure'))
.whenLabel('finalDml');
「fail on get when label is X」のように、ラベル多重化 Usecase で「この副作用だけ落としたい」ケースを表現できます。
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').failOnGet(exB).whenLabel('opp') | 'opp' が 2 回連続で失敗し、3 回目は成功 |
failOnGet(ex).whenLabel('opp').repeat() | 'opp' の呼び出しが毎回失敗 |
// 「1 回失敗 → リトライで成功」を書く
MockEloquent mock = (new MockEloquent())
.attach('opp', new List<IEntry>{ oppEntry })
.failOnGet(new QueryException('boom'))
.whenLabel('opp');
// 1 回目: 例外
// 2 回目: 失敗がラベルを解放しているので、attach したデータが返る
キューはラベルごとに独立しているので、同じメソッドに対してラベル違いの失敗を並べても、互いに干渉しません。
// 取得は 'fetch' で、保存は 'update' で、それぞれ別の理由で落とす
MockEloquent mock = (new MockEloquent())
.failOnGet(new QueryException('取得に失敗')).whenLabel('fetch')
.failOnDoUpdate(new DmlException('保存に失敗')).whenLabel('update');
これらに failSave (呼び出しは成功するが特定レコードだけ保存されない) を加えたものが、MockEloquent で表現できる失敗の全体です。
次に読む
- データ取得と DML、IEntry、Mock: 使い方ガイド
- API リファレンス: Scribe: クエリ組み立て側
- API リファレンス: IEntry / Entry / MockEntry: 返り値の
IEntry側 - Layered Constructor Pattern:
IEloquentを Usecase に DI する設計 - ApexEloquent ガイド: ガイド目次に戻る