ApexEloquent API Reference: IEloquent / Eloquent / MockEloquent
IEloquent is ApexEloquent's data-access contract (the interface). Production uses Eloquent; tests use MockEloquent, both DI'd via the Layered Constructor Pattern.
For usage and typical scenarios, see Data Access, DML, IEntry, and Mock.
This page covers both the v3 and v2 lines. The only difference between them is the execution mode, so that part is called out separately under "Execution Mode (v3 line)".
The Three Relationships
IEloquent (interface) ← the contract Usecase depends on
↑
├── Eloquent ← production. Hits standard SOQL / DML directly
└── MockEloquent ← tests. No DB; provides Spy + failOn*
The Usecase holds an IEloquent-typed field, into which it receives new Eloquent() in production and new MockEloquent(...) in tests via the Layered Constructor.
IEloquent (Interface)
Every contract method is implemented by both Eloquent and MockEloquent.
Labeling
| Signature | Return | Purpose |
|---|---|---|
label(String labelName) | IEloquent | Tags the next operation with a label so the chain stays composable. A single IEloquent can be multiplexed by label — fold "last-month / this-year / Account update" purposes onto one instance instead of DI-ing a separate IEloquent per purpose |
// Distinguish two SOQLs and one DML on the same IEloquent
IEntry job = this.eloquent.label('jobLoad').firstOrFail(jobScribe);
List<IEntry> details = this.eloquent.label('detailLoad').get(detailScribe);
this.eloquent.label('finalDml').doUpdate(toUpdate);
Passing null or blank to label(...) throws.
'default' is the name assigned internally to label-less operations. label('default') itself goes through, but it points at the same slot as an unlabelled call, so the intent doesn't read. Use a different name. (whenLabel('default'), on the other hand, is explicitly rejected as redundant.)
Opt-in strict mode: the first .label(...) call on an IEloquent instance switches it into strict mode (sticky for the rest of the instance's lifetime):
- Every subsequent operation must be preceded by
.label(...)(label-less operations throw) - Each label can be consumed at most once per instance (reusing throws)
- Instances that never call
.label()stay in lenient mode (full backward compatibility)
This closes the "I added a new DML and forgot to label it" footgun at runtime: the offending call throws instead of silently landing in the default bucket.
Execution Mode (v3 line only)
| Signature | Returns | Purpose |
|---|---|---|
userMode() | IEloquent | Returns a copy that runs SOQL / DML as AccessLevel.USER_MODE (the v3 default) |
systemMode() | IEloquent | Returns a copy that runs SOQL / DML as AccessLevel.SYSTEM_MODE, ignoring FLS and object permissions |
In the v3 line Eloquent became inherited sharing and SOQL / DML default to user mode (honouring the running user's field and object permissions). Only system processes — aggregation, stamping, data migration, anything that must complete no matter who triggered it — opt out explicitly with systemMode().
// A system process: mark the calling class without sharing, then be explicit here
this.eloquent.systemMode().label(LBL_UPDATE).doUpdate(entries);
- Use the return value (v3.9.0+): the receiver is left untouched; you get back a mode-switched copy. Calling
eloquent.systemMode();as a standalone statement changes nothing, so always chain off it (eloquent.systemMode().doX(...)) or reassign. This keeps a singlesystemMode()call from silently switching a sharedEloquentto system mode for everything that follows. A pendinglabel('x')travels with the copy (label('x').systemMode().doX()works) - A no-op on
MockEloquent: it simply returns itself, so unit tests can ignore the mode entirely (and every spy and assertion keeps pointing at the one instance the test holds) - ⚠️
systemMode()only lifts FLS and object permissions. Sharing (record visibility) is a separate axis — to lift that, the calling class must be declaredwithout sharing
Query Methods
| Signature | Return | Purpose |
|---|---|---|
get(Scribe scribe) | List<IEntry> | Execute the query. Empty list on 0 hits |
first(Scribe scribe) | IEntry | The first record. null on 0 hits |
firstOrFail(Scribe scribe) | IEntry | The first record. Throws ApexEloquentException on 0 hits |
firstOrFail(Scribe scribe, Exception orFail) | IEntry | The first record. On 0 hits, throws the exception you passed, as-is |
getAsSObject(Scribe scribe) | List<SObject> | SObject list (last resort) |
firstAsSObject(Scribe scribe) | SObject | SObject version of the first record |
firstOrFailAsSObject(Scribe scribe) | SObject | SObject version of the first record; throws on 0 hits |
rawSoql(String soql) | List<IEntry> | Bypass Scribe and run raw SOQL (last resort; SELECT-omission detection is disabled) |
When firstOrFail(scribe, orFail) earns its keep: when zero rows is a business error you want to surface to the screen. The exception you hand it is thrown verbatim, so you skip first + null check + your own throw.
What matters is where it gets caught. Hand it a business exception and it lands in your catch (UsecaseException).
IEntry job;
try {
job = this.eloquent.label(LBL_JOB).firstOrFail(
jobScribe,
new UsecaseException('The requested job was not found.')
);
} catch(UsecaseException ex) {
this.t.skip('Business error: ' + ex.getMessage());
}
The no-argument firstOrFail(scribe) throws ApexEloquentException, which never reaches that catch.
DML Methods
| Signature | Return |
|---|---|
doInsert(SObject record) | SObject |
doInsert(IEntry entry) | IEntry |
doInsert(List<SObject> records) | List<SObject> |
doInsert(List<IEntry> entries) | List<IEntry> |
doInsert(SObject record, Boolean allOrNone) | Database.SaveResult |
doInsert(IEntry entry, Boolean allOrNone) | Database.SaveResult |
doInsert(List<SObject> records, Boolean allOrNone) | List<Database.SaveResult> |
doInsert(List<IEntry> entries, Boolean allOrNone) | List<Database.SaveResult> |
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> |
doUpsert(SObject record, Boolean allOrNone) | Database.UpsertResult |
doUpsert(IEntry entry, Boolean allOrNone) | Database.UpsertResult |
doUpsert(List<SObject> records, Boolean allOrNone) | List<Database.UpsertResult> |
doUpsert(List<IEntry> entries, Boolean allOrNone) | List<Database.UpsertResult> |
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 |
doDelete(SObject record, Boolean allOrNone) | Database.DeleteResult |
doDelete(IEntry entry, Boolean allOrNone) | Database.DeleteResult |
doDelete(List<SObject> records, Boolean allOrNone) | List<Database.DeleteResult> |
doDelete(List<IEntry> entries, Boolean allOrNone) | List<Database.DeleteResult> |
All four methods come in the same four shapes (SObject / IEntry × single / list), each with an allOrNone variant (v3.10.0+ / v2.8.0+; earlier versions had no IEntry or allOrNone variants of doInsert, and no allOrNone variants of doUpsert / doDelete). Default to bulk; use the single-record versions only when exactly one record is guaranteed.
doInsert(IEntry) does not write the generated Id back into the caller's entry: read the Id from the returned IEntry. Pass allOrNone explicitly as true or false; null throws (the platform's Database API fails on a null Boolean too).
Return order (v3.6.0+ / v2.5.0+): every list DML method returns records and results in the caller's input order — results[i] always corresponds to records[i] / entries[i], at any list size. Earlier versions sorted lists of 10 or more records internally, which silently broke that index correspondence. When one list mixes multiple SObject types, group records of the same type together beforehand: Salesforce starts a new chunk at every type switch, and a single DML fails with an explicit error beyond 10 chunks.
The allOrNone variants: the return type is Database.SaveResult / UpsertResult / DeleteResult (or a list of them), so partial-success per-record outcomes can be inspected through the IEloquent abstraction. Useful in ETL / incremental sync / batch migration paths that log failed records and continue.
doUpsertByExternalId: brings the standard Database.upsert(records, externalIdField, allOrNone) API under the IEloquent abstraction. Essential for "upsert by external Id key" in ETL / incremental sync / batch migration, handled through a shared production / Mock contract. The key must be a field where isExternalId() or isIdLookup() is true (Id always qualifies). Fields such as Account.Name or Product2.ProductCode are rejected by the platform's Database.upsert, and since v3.10.0 / v2.8.0 MockEloquent rejects them as well.
Eloquent (Production)
The production class implementing IEloquent. It hits standard SOQL / DML directly. No additional public methods (interface-only).
IEloquent eloquent = new Eloquent();
List<IEntry> opps = eloquent.get(scribe);
eloquent.doUpdate(opps);
MockEloquent (Mock Extension)
On top of the IEloquent contract, it adds test-side Spy properties and the failOn series.
Constructors
| Signature | Behavior |
|---|---|
new MockEloquent() | Empty. If you use labels, feed data with attach(...) |
new MockEloquent(IEntry entry) | Preloads one record as the label-less ('default') result |
new MockEloquent(List<IEntry> entries) | Same, list version |
Data passed to the constructor only feeds label-less ('default') operations. If production code uses label(...), supply it with attach(label, ...) instead.
MockEloquent does not evaluate Scribe's WHERE conditions — it hands back the list you gave it, as-is. To tell condition-varied queries apart, label each query and feed them separately with attach(label, ...) (below).
An unattached label throws
Call get / first / firstOrFail under label('X') without a matching attach('X', ...) and the test throws. The error lists the labels that were attached, so a typo shows up immediately.
This guards a specific false positive: mistype a label, get zero rows, fall into the "nothing to do, skip" branch, and the test goes green while verifying nothing.
When you genuinely want to test the zero-row path, attach an empty list to say so.
MockEloquent mock = (new MockEloquent())
.attach(MyUsecase.LBL_FETCH, new List<IEntry>());
Upgrading from an older version can turn some tests red here. Those are the tests that were green while verifying nothing. Rather than mechanically adding empty attaches, check what data should have been injected in the first place.
Spy Properties and Per-Label Accessors
| Property / Method | Type | Content |
|---|---|---|
upsertedRecords | List<SObject> | Records passed to doInsert / doUpdate / doUpsert / doUpsertByExternalId for the 'default' bucket (※ @deprecated: new code should prefer upsertedRecordsAt('default')) |
deletedCount | Integer | Total doDelete invocations for the 'default' bucket (※ @deprecated: new code should prefer deletedCountAt('default')) |
upsertedRecordsAt(String label) | List<SObject> | Accumulated DML records for the given label |
upsertedEntriesAt(String label) | List<IEntry> | The same records as upsertedRecordsAt, wrapped as IEntry (v3.9.0+ / v2.7.0+) |
deletedCountAt(String label) | Integer | Delete invocation count for the given label |
scribeAt(String label) | Scribe | The query issued under the given label; null if none ran, throws if several did (v3.11.0+ / v2.9.0+) |
scribesAt(String label) | List<Scribe> | Every query issued under the given label, in execution order (v3.11.0+ / v2.9.0+) |
lastScribe() | Scribe | The most recent query, whatever label it ran under (v3.11.0+ / v2.9.0+) |
attach(String label, IEntry entry) | MockEloquent | Preload a single record as the given label's query data (chainable; re-attaching the same label overwrites) |
attach(String label, List<IEntry> entries) | MockEloquent | Same, list version |
failSave(Id recordId, String errorMessage) | MockEloquent | Make only the named record fail to save (chainable) |
failSaveByExternalId(String externalIdValue, String errorMessage) | MockEloquent | In doUpsertByExternalId, fail only the row whose external Id value matches (v3.9.0+ / v2.7.0+) |
failSaveWhen(Schema.SObjectField field, Object value, String errorMessage) | MockEloquent | Fail only the rows whose field value matches (v3.10.0+ / v2.8.0+) |
// Label-less, traditional usage (backward compatible)
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);
// Label-multiplexed — one MockEloquent serves query preloading and DML verification, sorted by label
MockEloquent mock = (new MockEloquent())
.attach('jobLoad', jobEntry);
(new FinalizeJobUsecase(jobId, mock)).invoke();
Assert.areEqual(1, mock.upsertedRecordsAt('finalDml').size());
Backward compatibility note: the existing upsertedRecords / deletedCount public fields continue to work and internally reflect the 'default' bucket. They are JSDoc-marked @deprecated; migrate to *At('default') over time. The legacy constructor new MockEloquent(List<IEntry>) also still works — the data it accepts feeds the 'default' source.
Verifying the query shape (scribeAt + assertCondition)
v3.11.0+ / v2.9.0+. MockEloquent does not evaluate WHERE; it returns whatever you attached. So a query that forgot to filter by its input still gets exactly the right data back in the test, and goes green. Only production touches every matching record in the org, and no amount of polishing the mock data will expose it.
What closes that hole is the record of issued queries (scribeAt / scribesAt / lastScribe) combined with condition assertions on Scribe.
MockEloquent mock = (new MockEloquent()).attach(MyUsecase.LBL_FETCH, contacts);
(new MyUsecase(contactIds, mock)).invoke();
mock.scribeAt(MyUsecase.LBL_FETCH)
.assertCondition(new InConditionClause('Id', contactIds)) // filters by the input Ids
.assertNoCondition(new EqualConditionClause('Archived__c', false)); // no widening condition attached
Scribe method (all @TestVisible) | Purpose |
|---|---|
assertCondition(clause) / assertNoCondition(clause) | Verify conditions on the queried object (including inside whereGroup) |
assertConditionAt(path, clause) / assertNoConditionAt(path, clause) | Verify conditions at a location named by path |
hasCondition(clause) / hasConditionAt(path, clause) | The same check as a Boolean (for your own messages) |
conditionAssertionMessage(clause, shouldExist) | The exact message an assertion failure would produce |
Write the expectation as an ordinary condition clause (new InConditionClause('Id', ids) / new EqualConditionClause('Type', 'Partner') and so on). Field, operator and value are pinned in one go, and both sides are built from the same metadata, so this is not a string comparison of toSoql() output.
Condition clauses map one-to-one onto Scribe's where* methods. Pick the one that matches the method the production code used.
| How production code writes it | Condition clause for the expectation |
|---|---|
whereEqual(f, v) | EqualConditionClause |
whereNotEqual(f, v) | NotEqualConditionClause |
whereEqual(f, null) / whereNull(f) | IsNullConditionClause |
whereNotEqual(f, null) / whereNotNull(f) | IsNotNullConditionClause |
whereIn(f, values) / whereNotIn(f, values) | InConditionClause / NotInConditionClause |
whereIn(f, scribe) / whereNotIn(f, scribe) (semi-join) | InSubQueryConditionClause / NotInSubQueryConditionClause |
whereGreaterThan / whereGreaterThanOrEqual | GreaterThanConditionClause / GreaterThanOrEqualConditionClause |
whereLessThan / whereLessThanOrEqual | LessThanConditionClause / LessThanOrEqualConditionClause |
whereLike / whereNotLike | LikeConditionClause / NotLikeConditionClause |
whereIncludes / whereExcludes (multi-select picklist) | IncludesConditionClause / ExcludeConditionClause |
- Every constructor is test-only (
@TestVisible) and comes in two forms:(field, value)and(Object.class, field, value). The value is a collection forIn/NotIn, aScribefor the semi-joins, aList<String>forIncludes/Excludes, and absent forIsNull/IsNotNull((field)/(Object.class, field)) - ⚠️ A comparison with null becomes a different clause.
whereEqual('Email', null)is built as anIsNullConditionClause, so write the expectation asnew IsNullConditionClause('Email').new EqualConditionClause('Email', null)will not match - Conditions inside a semi-join are checked with
assertConditionAt(field, ...), using the field name you passed towhereInas the path
- Paths: walk one level at a time — a parent by the field name passed to
Scribe.asParent(...), a child by itsrelationName(...)(or the child object name if none), a semi-join by the field name passed towhereIn(field, Scribe)(e.g.assertConditionAt('ParentId.ParentId', ...)).whereGroupdoes not count as a level - Parent conditions: name the object, as in
new EqualConditionClause(Account.class, 'Type', 'Partner'), and the location is found for you. If the same object appears in two or more places it throws rather than guess; name the location with a path - What is verified is "the query has this condition", not "the query is narrowed by it". A condition joined via
orCondition()still matches. To keep a query from widening toward all rows, forbid the widening condition withassertNoCondition SetandListare distinguished. ASetis compared with its order normalised; aListis compared in the order written, so match the type your production code passes- An empty collection is a legitimate expectation. A branch that dropped the condition entirely via
ignoreWhen(ids.isEmpty())can be verified as "not attached" withassertNoCondition(new InConditionClause('Id', ids)) - A misspelled field name in the expectation throws as soon as it matches nowhere in the query (otherwise
assertNoConditionwould pass forever).HAVINGis not searched - Failures are
System.AssertException. Apex cannot catch it, so a test that wraps its Act incatch (Exception e)cannot swallow the assertion scribeAtthrows when several queries were recorded under the same label. When you read several times without a label, or retry a failed read under the same label (queries are recorded when issued), usescribesAt. A branch that issues no query can be verified withAssert.isNull(mock.scribeAt(LBL))
failOn Series (Exception Simulation)
Each method has two overloads: no-argument and Exception-accepting. Without an Exception, a default exception is thrown.
| Method | Corresponding Contract |
|---|---|
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(*) |
Scoping Failures to a Label with whenLabel()
| Signature | Purpose |
|---|---|
whenLabel(String label) | Scopes the most recent failOn*() to fire only for a specific label (chainable) |
// Only the DML labeled 'finalDml' fails; the others run normally
MockEloquent mock = (new MockEloquent())
.failOnDoUpdate(new DmlException('Simulated finalDml failure'))
.whenLabel('finalDml');
Reads as "fail on get when label is X" — useful in label-multiplexed Usecases where you want exactly one side effect to fail.
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());
}
Expressing "Always Fails" with repeat()
| Signature | Purpose |
|---|---|
repeat() | Call after a failOn* to repeat the failure forever afterward (count cannot be specified) |
failOn() alone fails just the next call. failOn entries queue up per method, so putting two in a row fails the first and second call and lets the third succeed (the retry-then-succeed path).
Add .repeat() and the entry you just planted fails on every subsequent invocation. That is how you check the abort side of "retry up to 3 times, then give up" — without counting calls.
Queues are kept per label. A configuration without whenLabel targets label-less calls ('default').
⚠️ Every test-setup builder (attach / failOn* / whenLabel / failSave / repeat) returns a new instance rather than mutating this. Drop the return value and the configuration is lost, so keep the chain or reassign.
💡 The once-per-label rule counts successful operations. A failed operation releases its label, so production code that catches and retries under the same label tests just fine.
⚠️ In a test that uses labels, forgetting whenLabel leaves the configuration aimed at 'default', where it can never fire. That is detected and reported as an exception, naming the label you should have used.
The failure patterns you can express
Combining failOn* / whenLabel / repeat gives five shapes.
| Written as | What happens |
|---|---|
failOnGet(exA).failOnGet(exB) | Fails with exA, then exB, and succeeds on the third call |
failOnGet(ex).repeat() | Fails every time |
failOnGet(ex).whenLabel('opp') | 'opp' fails once; a retry succeeds |
failOnGet(exA).whenLabel('opp').failOnGet(exB).whenLabel('opp') | 'opp' fails twice in a row, succeeds on the third |
failOnGet(ex).whenLabel('opp').repeat() | 'opp' fails every time |
// "fail once, succeed on retry"
MockEloquent mock = (new MockEloquent())
.attach('opp', new List<IEntry>{ oppEntry })
.failOnGet(new QueryException('boom'))
.whenLabel('opp');
// 1st call: throws
// 2nd call: the failure released the label, so the attached data comes back
Because queues are per label, failures aimed at different labels on the same method never interfere.
MockEloquent mock = (new MockEloquent())
.failOnGet(new QueryException('fetch failed')).whenLabel('fetch')
.failOnDoUpdate(new DmlException('save failed')).whenLabel('update');
Add the failSave family (failSave / failSaveByExternalId / failSaveWhen) — where the call succeeds but a specific record is not saved — and that is the whole of what MockEloquent can express.
failSave(): partial save failure
failSave differs in kind from failOn*. Rather than throwing, the call succeeds while the named record is left unsaved — the partial failure of an allOrNone DML.
Id badId = MockEntry.of(Account.class).autoId(2).getId();
MockEloquent mock = (new MockEloquent())
.failSave(badId, 'Rejected by a validation rule');
allOrNone = false— that record'sSaveResultcarriessuccess = falseand your message, and it is not recorded by the spy (it was never saved)allOrNone = true— as with real all-or-nothing DML, the whole operation throws before anything is recorded
The target is named by record Id, not index, so pair it with MockEntry.autoId(). It survives records being added later.
Failing rows that have no Id: because failSave names its target by Id, it cannot reach rows that do not have one yet (new rows in doInsert(..., false), the create side of doUpsert(..., false), or rows not yet in the org under doUpsertByExternalId). Name those by value instead.
// doUpsertByExternalId: name the row by its external Id value (v3.9.0+ / v2.7.0+)
MockEloquent mock = (new MockEloquent())
.failSaveByExternalId('EXT-002', 'Rejected as a duplicate');
// Any DML: name rows by the value of any field (v3.10.0+ / v2.8.0+)
MockEloquent mock2 = (new MockEloquent())
.failSaveWhen(Account.Name, 'doomed', 'Rejected by a validation rule');
List<Database.SaveResult> results = mock2.doInsert(records, false);
// Only rows whose Name is 'doomed' have isSuccess() == false; the others are saved
- When several specifications hit the same row, they are evaluated in the order
failSave(Id) →failSaveByExternalId→failSaveWhen(not registration order) failSaveWhencompares with Apex==. Strings are case-insensitive, and passingnullmatches every row where that field is unset- The failed result's
getStatusCode()is fixed (FIELD_CUSTOM_VALIDATION_EXCEPTION, orDELETE_FAILEDfor deletes). Code that branches on the status code cannot be tested with thefailSavefamily
Read Next
- Data Access, DML, IEntry, and Mock: the usage guide
- API Reference: Scribe: the query-assembly side
- API Reference: IEntry / Entry / MockEntry: the
IEntryside that's returned - Layered Constructor Pattern: the design for DI'ing
IEloquentinto a Usecase - ApexEloquent Guide: back to the guide index