# Data Access, DML, IEntry, and Mock

This document is a usage guide focused on **how to write ApexEloquent's data-access layer in production code**. For the full API signatures, see [API Reference: IEloquent / Eloquent / MockEloquent](/apex-stem/docs/apex-eloquent-api-eloquent) and [API Reference: IEntry / Entry / MockEntry](/apex-stem/docs/apex-eloquent-api-entry).

For how to build queries, see [Building Queries with Scribe](/apex-stem/docs/apex-eloquent-scribe-guide). For the rest of the ApexEloquent topics, see the [ApexEloquent Guide](/apex-stem/docs/apex-eloquent-guide).

## What IEloquent Is

`IEloquent` is the interface that abstracts data access (SOQL / DML). Production uses `Eloquent` (a wrapper over standard SOQL / DML); tests use `MockEloquent` (a behavior mock without a DB) — both are swapped in without changing the calling code.

```apex
public with sharing class FindActiveOpportunitiesUsecase {
  private final IEloquent fetchEloquent;

  public FindActiveOpportunitiesUsecase() {
    this(null);
  }

  @TestVisible
  private FindActiveOpportunitiesUsecase(IEloquent fetchEloquent) {
    this.fetchEloquent = fetchEloquent ?? new Eloquent();
  }

  public List<IEntry> invoke() {
    Scribe scribe = Scribe.of(Opportunity.class)
      .field('Id')
      .field('Name')
      .whereEqual('IsClosed', false);
    return this.fetchEloquent.get(scribe);
  }
}
```

For "two constructors that simultaneously support a production default and test-side DI", see [Layered Constructor Pattern](/apex-stem/docs/layered-constructor-pattern).

## Data Retrieval

### Retrieval Methods

In business logic, the default is **`get(scribe)` returning `List<IEntry>`**. 0 hits yields an empty list. When you want just one record, `first` (returns `null` on 0 hits) or `firstOrFail` (throws on 0 hits) is handy.

`getAsSObject` conversion is a last resort — use it only when you have a concrete reason to hand off an `SObject` instance (e.g. directly passing it to an external API like `Messaging.SingleEmailMessage`). Similarly, `rawSoql(soql)` is the last resort for SOQL that `Scribe` can't express; it disables the `MockEntry` SELECT-omission detection.

For the full list of signatures, see [API Reference: IEloquent](/apex-stem/docs/apex-eloquent-api-eloquent).

### The Benefits of Staying on IEntry

`IEntry` wraps `SObject` and gives you four benefits unique to ApexEloquent.

1. **False-positive detection for unselected fields**: if production code accesses a field that wasn't called out with `field()` in the `Scribe`, an exception fires at test time (via `MockEntry`). Early conversion to `SObject` strips that protection, and the classic "tests pass but production reads empty" incident creeps back in.
2. **Freedom in building mock data**: `MockEntry.set()` accepts non-writable fields — relationship fields, formula fields, rollups, auto-number — that you can't normally write. When the logic depends on these, test data you can't build with raw `SObject` becomes trivial via `IEntry`.
3. **Retrieve → edit → update flows stay on IEntry**: mutate with `entry.put('Industry__c', value)`, hand it straight to `eloquent.doUpdate(List<IEntry>)`. No conversion to `SObject` needed.
4. **No distinction between SObject and AggregateResult**: results of normal queries and aggregate queries (`AggregateResult`) both come back as `IEntry`. `entry.get('Industry')` for **an object field**, `entry.get('totalAmount')` for **an aggregate alias** — same shape. The consumer doesn't have to care whether it's an `SObject` or `AggregateResult`.

## Working with IEntry

### Reading and Writing Fields

```apex
List<IEntry> accountEntries = eloquent.get(accountScribe);

for (IEntry accountEntry : accountEntries) {
  // Id and Name have dedicated getters (no cast needed)
  Id accountId = accountEntry.getId();
  String name = accountEntry.getName();

  // Other fields require a cast
  String industry = (String) accountEntry.get('Industry');
  Boolean isActive = (Boolean) accountEntry.get('Active__c');

  // Writing
  accountEntry.put('Industry', 'Technology');
}
```

`getId` / `getName` are dedicated getters that don't need casting. Other fields go through `get(fieldName)` and require a cast on the return. Writes are `put(fieldName, value)`. For parent and child record access see [Parent Fields, Child Subqueries, and Many-to-Many](/apex-stem/docs/apex-eloquent-relations); for signatures see [API Reference: IEntry](/apex-stem/docs/apex-eloquent-api-entry).

### Variable Naming Tips

When using abstracted types (`IEntry` / `Scribe` / `IEloquent`), **let the variable name make the underlying SObject explicit** — it reads better.

```apex
// Bad: short names like r or e force the reader to chase the type
for (IEntry e : eloquent.get(scribe)) { ... }

// Good: consistent {SObjectName}Entry form
for (IEntry accountEntry : eloquent.get(accountScribe)) { ... }
```

## Running DML

`IEloquent` also wraps standard DML. The four methods `doInsert` / `doUpdate` / `doUpsert` / `doDelete` each have `SObject` / `IEntry` / `List<SObject>` / `List<IEntry>` overloads.

```apex
// IEntry retrieved via Scribe can be updated as-is
List<IEntry> oppEntries = this.eloquent.label(LBL_FETCH).get(oppScribe);
for (IEntry oppEntry : oppEntries) {
  oppEntry.put('Industry__c', 'Technology');
}
this.eloquent.label(LBL_UPDATE).doUpdate(oppEntries);
```

Default to the `List<>` versions to keep things bulk-aligned. Single-record versions are for cases where exactly one record is guaranteed. For each overload's signature, see [API Reference: IEloquent](/apex-stem/docs/apex-eloquent-api-eloquent).

### Execution Mode (v3 line)

In the v3 line, SOQL / DML default to **user mode** — honouring the running user's field and object permissions. Only work that must complete no matter who triggered it (aggregation, stamping, data migration) opts out explicitly with `systemMode()`.

```apex
this.eloquent.systemMode().label(LBL_UPDATE).doUpdate(entries);
```

Calling it once applies to every subsequent operation on that instance (sticky). On `MockEloquent` it is a no-op, so **unit tests can be written without thinking about the mode**.

⚠️ `systemMode()` only lifts **field and object permissions**. **Sharing (record visibility) is a separate axis** — to lift that, the calling class must be declared `without sharing`.

## Testing with MockEloquent

`MockEloquent` is the test implementation of `IEloquent`. By DI-ing it into a Usecase via the Layered Constructor Pattern, you can verify behavior without ever touching the DB.

### Constructors

`new MockEloquent()` for empty, `new MockEloquent(entry)` to return one record, `new MockEloquent(List<IEntry>)` to return multiple.

```apex
MockEntry oppEntry = MockEntry.of(Opportunity.class)
  .autoId(1)
  .set('Name', 'Test Opp')
  .set('Amount', 1000);
IEloquent fetchEloquent = new MockEloquent(new List<IEntry>{ oppEntry });
```

### Verifying DML with the Spy

Instead of really running `doInsert` / `doUpdate` / `doUpsert` / `doDelete`, `MockEloquent` **records the records it was handed**. Tests can then check after the fact that the DML you expected actually happened.

| Method | Content |
|---|---|
| `upsertedRecordsAt(String label)` | Records passed to `doInsert` / `doUpdate` / `doUpsert` under that label (`List<SObject>`) |
| `deletedCountAt(String label)` | `doDelete` count under that label (`Integer`) |

> In tests that don't use labels (lenient), pass `'default'`. The `upsertedRecords` / `deletedCount` **fields** remain for backward compatibility but are `@deprecated`; new code should use the `*At(...)` methods.

```apex
MockEloquent mock = (new MockEloquent())
  .attach(CopyAccountIndustryToOpportunityUsecase.LBL_FETCH, new List<IEntry>{ oppEntry });

(new CopyAccountIndustryToOpportunityUsecase(oppIds, mock)).invoke();

// Did exactly 1 update happen as expected?
List<SObject> updated = mock.upsertedRecordsAt(CopyAccountIndustryToOpportunityUsecase.LBL_UPDATE);
Assert.areEqual(1, updated.size());

// Did the expected value land?
Assert.areEqual('Technology', ((Opportunity) updated[0]).Industry__c);
```

### Mocking failure paths

In Salesforce, **reproducing a failure costs an order of magnitude more than reproducing success**. To make real DML actually fail you have to add a validation rule, strip a field permission, contend for a row lock. It is slow, and because it leans on org state, it breaks easily.

`MockEloquent` lets you **declare the failure by name** instead.

#### Failure is specified along four axes

The `failOn*` family looks like a lot of methods, but it is really a combination of four axes. Once you see them, reading and writing gets much easier.

| Axis | What you specify | Default |
|---|---|---|
| **What** fails | `failOnDoUpdate()` / `failOnGet()` / … one per method | — |
| **Why** it fails | the `Exception` you pass | a generic test exception |
| **Where** it fails | `.whenLabel(label)` | aimed at label-less calls (`'default'`) |
| **How often** it fails | stack `failOn*` / `.repeat()` | just the next call |

The four are orthogonal; add only what you need.

```apex
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('Simulated failure'))  // what + why
  .whenLabel(YourUsecase.LBL_UPDATE);                     // where

try {
  (new YourUsecase(input, mock)).invoke();
  Assert.fail('An exception should have been thrown');
} catch (DmlException e) {
  Assert.isTrue(TraceFlow.isLastAbort());
}
```

#### Why narrowing "where" matters

With `whenLabel` you drop **just that one call site** and let the rest run normally. "Aggregation succeeded, only the final save failed" writes out directly. In a Usecase firing several DMLs, without this a test can't tell you which one fell over.

A configuration without `whenLabel` is **aimed at label-less calls (`'default'`)**. If production code uses labels, `whenLabel` is effectively mandatory (forgetting it is detected — see below).

A call whose label doesn't match **does not consume the configuration**. A later matching call picks it up, so ordering doesn't matter when you plant them.

#### "How often" stacks

`failOn*` entries **queue up**. Plant several against the same method and they fail in order.

```apex
// The 1st and 2nd doUpdate fail; the 3rd succeeds
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('first'))
  .failOnDoUpdate(new DmlException('second'));
```

That is how you write the success side of a retry. Queues are independent per label, so entries carrying `whenLabel` stack the same way.

`.repeat()`, by contrast, repeats the entry you just planted **forever** (no count).

```apex
// Fails no matter how many times it's called
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('Always fails'))
  .whenLabel(YourUsecase.LBL_UPDATE)
  .repeat();
```

Use it for the **abort side** of "retry up to 3 times, then give up". Not having to count calls makes the intent clearer.

#### Queues are per label

Failure configurations queue up **per label** (an entry without `whenLabel` is aimed at label-less calls, `'default'`).

So you can plant **different failures against different labels** on the same method, in any order.

```apex
// Fail the fetch for one reason and the save for another
MockEloquent mock = (new MockEloquent())
  .failOnGet(new QueryException('fetch failed')).whenLabel(YourUsecase.LBL_FETCH)
  .failOnDoUpdate(new DmlException('save failed')).whenLabel(YourUsecase.LBL_UPDATE);
```

#### Retries can keep the same label

The "once only" rule on labels counts **successful** operations. A **failed operation releases its label**, so production code that catches and retries under the same label tests as-is.

```apex
// Fails the first time, succeeds the second — same label throughout
MockEloquent mock = (new MockEloquent())
  .attach(YourUsecase.LBL_UPDATE, entries)
  .failOnDoUpdate(new DmlException('first attempt')).whenLabel(YourUsecase.LBL_UPDATE);
```

Add `.repeat()` and it keeps failing however many times you retry, which is how you check the "give up at the limit" path.

#### Pitfall: keep the return value

The test-setup builders (`attach` / `failOn*` / `whenLabel` / `failSave` / `repeat`) all **return a new instance rather than mutating `this`**. Drop the return value and the configuration disappears.

```apex
// ❌ Does nothing — mock itself has no configuration on it
MockEloquent mock = new MockEloquent();
mock.failOnDoUpdate(new DmlException('...'));

// ✅ Keep the chain, or reassign
MockEloquent mock = (new MockEloquent())
  .attach(YourUsecase.LBL_FETCH, entries)
  .failOnDoUpdate(new DmlException('...'))
  .whenLabel(YourUsecase.LBL_UPDATE);
```

#### Pitfall: a forgotten whenLabel is detected

In a test that uses labels (strict mode), forgetting `whenLabel` aims the configuration at label-less calls. But strict mode requires a label on every operation, so **that configuration can never fire**.

Silently doing nothing would fail in a baffling way — the exception never arrives, so your `Assert.fail` trips instead. So it throws a diagnostic exception on the spot, naming the label you should have used.

#### failSave: not an exception, just "some of it wasn't saved"

Where `failOn*` means "calling it throws", `failSave` is a different kind of failure: **the call itself succeeds but the named record is not saved**. It reproduces the partial failure of an `allOrNone` DML.

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

MockEloquent mock = (new MockEloquent())
  .failSave(badId, 'Rejected by a validation rule');
```

- `allOrNone = false` — that record's `SaveResult` carries `success = false` and 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** rather than index, so pair it with `MockEntry.autoId()`. Because it doesn't depend on list order, it survives records being added later.

You can verify ETL / incremental-sync paths that "log the failed records and carry on" without touching org configuration.

> For the full list of `failOn*` methods, see [API Reference: MockEloquent](/apex-stem/docs/apex-eloquent-api-eloquent).

### Distinguishing Multiple Queries on the Same IEloquent: Label Multiplexing (Important)

`MockEloquent` **does not evaluate** the `Scribe`'s WHERE conditions. It returns the injected `IEntry` list as-is. This means cases like "I want one `IEloquent` to distinguish between `last month's query` and `this year's query`" **can't be handled out of the box**.

**`IEloquent.label(String)`**, added in v2.1, resolves this. By **multiplexing** a single `IEloquent` with labels, you can use the same instance — sorted by label — instead of DI-ing a separate `IEloquent` per purpose.

```apex
public with sharing class AggregateAccountActivityUsecase {
  @TestVisible static final String LBL_LAST_MONTH = 'lastMonthEvent';
  @TestVisible static final String LBL_THIS_YEAR = 'thisYearEvent';
  @TestVisible static final String LBL_ACCOUNT_UPDATE = 'accountUpdate';

  private final Set<Id> accountIds;
  private final IEloquent eloquent;

  public AggregateAccountActivityUsecase(Set<Id> accountIds) {
    this(accountIds, null);
  }

  @TestVisible
  private AggregateAccountActivityUsecase(
    Set<Id> accountIds,
    IEloquent eloquent
  ) {
    this.accountIds = accountIds;
    this.eloquent = eloquent ?? new Eloquent();
  }

  public void invoke() {
    List<IEntry> lastMonthEvents = this.eloquent.label(LBL_LAST_MONTH).get(lastMonthScribe);
    List<IEntry> thisYearEvents = this.eloquent.label(LBL_THIS_YEAR).get(thisYearScribe);
    // ... aggregate ...
    this.eloquent.label(LBL_ACCOUNT_UPDATE).doUpdate(updatedAccounts);
  }
}
```

In tests, preload each label's query result on a single `MockEloquent` via `.attach(LBL_LAST_MONTH, ...)` / `.attach(LBL_THIS_YEAR, ...)`, and pull DML outcomes via `mock.upsertedRecordsAt(LBL_ACCOUNT_UPDATE)`. You preserve the same per-purpose isolation as separate-DI, but the constructor signature collapses to a single dependency.

Note: the first time `.label()` is called, **opt-in strict mode** turns on for that instance, and every subsequent operation must carry a label (the "forgot to label" footgun is closed at runtime). Details in [API Reference: IEloquent](/apex-stem/docs/apex-eloquent-api-eloquent).

## Building MockEntry

`MockEntry` is the test-side implementation of `IEntry`. It lets you build test data that you can't build with raw `SObject`.

### Basics

```apex
MockEntry oppEntry = MockEntry.of(Opportunity.class)
  .autoId(1)
  .set('Name', 'Test Opp')
  .set('Amount', 1000);
```

`MockEntry.of(Type)` creates the entry. `.set` writes to a field (non-writable fields are allowed). `.autoId` auto-generates an 18-character Id. `.alias` names the entry so you can retrieve the Id later. For the signature list, see [API Reference: MockEntry](/apex-stem/docs/apex-eloquent-api-entry).

### Baking in the SELECT contract with fetchedBy

Entries returned through `MockEloquent`'s `attach` **pick up a SELECT contract automatically** from the `Scribe` passed to `get(scribe)` — the mechanism that throws when you touch a field you never selected.

**Entries handed straight to the SUT have no contract**, though: they never went through a `Scribe`, so there is no way for them to know which fields count as selected. `fetchedBy` attaches one after the fact.

```apex
MockEntry card = MockEntry.of(BusinessCard__c.class)
  .autoId(1)
  .set('CompanyName__c', 'Acme')
  .fetchedBy(RematchCompanyCardsHandler.scope());
```

This matters mostly for **batches**. Records reaching `execute(bc, scope)` never pass through `IEloquent`, so production is covered by the platform (they are real query results) while tests check nothing — you built those entries yourself. Pull the query construction into a `@TestVisible` method and the test can hand over the same `Scribe` production uses (see [API Reference: Scribe](/apex-stem/docs/apex-eloquent-api-scribe)).

### Retrieving Generated Ids with alias

Sometimes you want to use the Id generated by `autoId` in a test assertion.

```apex
MockEntry oppEntry = MockEntry.of(Opportunity.class)
  .alias('opp')
  .autoId(1);

Id oppId = oppEntry.getAliasId('opp');  // pull out the generated Id

// In the test:
Opportunity updated = (Opportunity) mock.upsertedRecordsAt(MyUsecase.LBL_UPDATE)[0];
Assert.areEqual(oppId, updated.Id);
```

### Bulk-Generating with times()

```apex
List<MockEntry> contactEntries = MockEntry.of(Contact.class)
  .autoId('{#}')
  .set('LastName', 'Contact-{#}')
  .alias('con_{#}')
  .times(3);
// → 3 records: con_1 / con_2 / con_3, each with Id and LastName expanded
```

### Setting Multiple Fields at Once with template

When you have "default values to reuse across multiple tests", hand them over as a `Map` to avoid repeating `.set` in every test.

```apex
Map<String, Object> defaults = new Map<String, Object>{
  'StageName' => 'Prospecting',
  'CloseDate' => Date.today().addDays(30),
  'Amount' => 1000
};

MockEntry oppEntry = MockEntry.of(Opportunity.class)
  .template(defaults)
  .set('Name', 'Specific name for this test');  // override or add per-field
```

### Mocking Aggregate Results

When mocking aggregate-query results, use `MockEntry.asAggregateResult()`. This reflects the property that "aggregate results aren't tied to an SObject type", removing the need to pick a specific SObject type via `MockEntry.of(SomeType.class)` (and reducing cognitive noise for the reader).

```apex
MockEloquent eventEloquent = new MockEloquent(
  new List<IEntry>{
    MockEntry.asAggregateResult()
      .set('WhatId', oppAId)
      .set('eventCount', 3),
    MockEntry.asAggregateResult()
      .set('WhatId', oppBId)
      .set('eventCount', 1)
  }
);
```

Passing `Decimal` values to `set` (same as real SOQL aggregate results) means the logic-side `((Decimal) entry.get('eventCount')).intValue()` cast keeps working as-is.

### Mocking Parent-Child Structures

To view parent from a child, use `setParent`. To view children from a parent, use `setChildren`. The code's indentation directly mirrors the relation structure — re-reading later, "what children hang off this parent" is visible at a glance.

```apex
// Hang multiple Contact / Opportunity under a parent Account
MockEntry accountEntry = MockEntry.of(Account.class)
  .alias('acc').autoId(1)
  .set('Name', 'Acme Corporation')
  .setChildren('Contacts', new List<MockEntry>{
    MockEntry.of(Contact.class).autoId(1).set('FirstName', 'John'),
    MockEntry.of(Contact.class).autoId(2).set('FirstName', 'Jane')
  })
  .setChildren('Opportunities', new List<MockEntry>{
    MockEntry.of(Opportunity.class).autoId(1).set('Name', 'Deal 1').set('Amount', 100000),
    MockEntry.of(Opportunity.class).autoId(2).set('Name', 'Deal 2').set('Amount', 150000)
  });
```

The reverse (viewing parent Account from child Opportunity) uses `setParent`:

```apex
MockEntry oppEntry = MockEntry.of(Opportunity.class)
  .autoId(1)
  .set('Name', 'Major Deal')
  .setParent('AccountId',
    MockEntry.of(Account.class).set('Name', 'Acme Corporation').set('Type', 'Customer')
  );
```

The Id linkage between parent and child (e.g. filling `Contact.AccountId` with the parent's Id) is handled internally by `MockEntry` — you don't fill it by hand.

⚠️ If you specified the child relationship name in `Scribe` via `relationName('CustomOpportunities__r')`, use the same string as the first argument of `setChildren`. For the full picture of relation operations (including many-to-many and junction objects), see [Parent Fields, Child Subqueries, and Many-to-Many](/apex-stem/docs/apex-eloquent-relations).

## Read Next

- [Parent Fields, Child Subqueries, and Many-to-Many](/apex-stem/docs/apex-eloquent-relations): retrieving and mocking relations
- [Building Queries with Scribe](/apex-stem/docs/apex-eloquent-scribe-guide): the full picture of the query builder
- [Layered Constructor Pattern](/apex-stem/docs/layered-constructor-pattern): the design for DI'ing `IEloquent` by purpose
- [Test Strategy](/apex-stem/docs/test-strategy): where Usecase unit tests centered on `MockEloquent` sit
- [ApexEloquent Guide](/apex-stem/docs/apex-eloquent-guide): back to the ApexEloquent Guide index
