ApexEloquent API Reference: IEntry / Entry / MockEntry
IEntry is ApexEloquent's record wrapper (the interface). The return value of Eloquent.get(scribe) and similar is List<IEntry>. Production uses Entry; tests use MockEntry.
For usage and typical scenarios, see Data Access, DML, IEntry, and Mock and Parent Fields, Child Subqueries, and Many-to-Many.
The Three Relationships
IEntry (interface) ← the type handled by Usecases / business logic
↑
├── Entry ← production. Wraps SObject or AggregateResult
└── MockEntry ← tests. Not bound to an SObject type; any field can be set
IEntry is an SObject wrapper, and via MockEntry.set it can express test data unreachable from raw SObject (writes to formula fields, rollups, parent relationships, auto-number). AggregateResult also works through the same IEntry interface, so the consumer-side code stays uniform between SObject queries and aggregate queries.
IEntry (Interface)
Field Access
| Signature | Return | Purpose |
|---|---|---|
get(String fieldName) | Object | Read any field's value (cast required) |
put(String fieldName, Object value) | void | Write a value |
getId() | Id | Dedicated Id getter (no cast) |
getName() | String | Dedicated Name getter (no cast) |
getRecord() | SObject | Extract the wrapped SObject (last resort) |
setRecord(SObject record) | IEntry | Internal use. Swap the SObject |
setFieldStructure(FieldStructure fs) | IEntry | Internal use. Set the SELECT-omission detection schema |
setDescribeResult(Schema.DescribeSObjectResult) | void | Internal use |
IEntry oppEntry = eloquent.first(oppScribe);
Id oppId = oppEntry.getId();
String name = oppEntry.getName();
String industry = (String) oppEntry.get('Industry__c');
oppEntry.put('Status__c', 'Active');
Relations (reading)
| Signature | Return | Purpose |
|---|---|---|
getParent(String parentIdFieldName) | IEntry | Get the parent record (e.g. 'AccountId') |
getChildren(String name) | List<IEntry> | Child record list |
getThrough(String junctionName, String relatedKey) | List<IEntry> | Many-to-many. Get the related target via a junction object |
The first argument of getChildren / getThrough resolves as either an object name or a relationship name. It looks for an object name first and falls back to a relationship name.
// Both give the same result
List<IEntry> opps = accountEntry.getChildren('Opportunity'); // object name
List<IEntry> opps = accountEntry.getChildren('Opportunities'); // relationship name
If you declared relationName(...) on the Scribe side, that name works too.
Deprecated: *ByRelationName
| Signature | Use instead |
|---|---|
getChildrenByRelationName(String childRelationName) | getChildren(String) |
getThroughByRelationName(String junctionRelationName, String relatedKey) | getThrough(String, String) |
Now that name resolution is unified, a relationship-name-only entry point is unnecessary. They remain for backward compatibility, but do not use them in new code — they may be removed in a future version.
Entry (Production)
The production class implementing IEntry. Supports both SObject-derived and AggregateResult-derived modes, and internally enforces SELECT-omission detection based on the Scribe's selected fields. No additional public methods.
Each element in the return value of Eloquent.get(scribe) is an Entry instance.
Safe to carry on batch state (v3.4.1+)
You can hold IEntry as an instance variable on a Database.Stateful batch.
⚠️ Through v3.4.0 this could throw
SerializationExceptionwhen crossing chunks, because theSchema.DescribeSObjectResultheld internally is not serializable. Worse, whether it fired depended on cache warm-up — only the first cache-miss instance for a type carried the value — so identical code passed or failed seemingly at random. v3.4.1 made the fieldtransientand re-derives it on demand.
MockEntry (Mock Extension)
The test-side implementation of IEntry. On top of the IEntry contract, it adds many extension APIs for building test data.
Factories
| Signature | Purpose |
|---|---|
MockEntry.of(System.Type recordType) | Create an entry for a normal SObject type (MockEntry.of(Account.class)) |
MockEntry.asAggregateResult() | Create an entry for aggregate-query results (not bound to an SObject type) |
MockEntry.asAggregateResult(Map<String, Object> fieldToValue) | Same as above, with initial values |
Field Operations
| Signature | Return | Purpose |
|---|---|---|
set(String fieldName, Object value) | MockEntry | Set a field value (non-writable fields are allowed) |
template(Map<String, Object> fieldToValue) | MockEntry | Set multiple fields at once via a Map |
MockEntry accEntry = MockEntry.of(Account.class)
.template(new Map<String, Object>{
'Name' => 'Acme Co.',
'Industry' => 'Technology'
})
.set('Active__c', true);
Immediate detection of SObject field-name typos: passing a non-existent SObject field name to set / add / setParent / addParent throws ApexEloquentException immediately.
MockEntry.of(Account.class).set('Naame', 'foo');
// → ApexEloquentException ("The field 'Naame' does not exist on the SObject.")
Previously, the typo would slip silently into fieldToValue; when the SUT later asked for the correct name (get('Name')), it received null and followed the null branch — passing the test while failing in production. False positives like this had a structural home. Now they are caught the moment you write the test setup, closing off setup-origin false positives by structure.
Note that put already runs field-name validation through standard SObject.put, so typos are caught there too — no behavior change for put.
sethas a synonym,add(String, Object). They behave identically (both return a newMockEntry), so keep new code onset.setParent/setChildrenlikewise haveaddParent/addChildren.
Baking in the SELECT contract: fetchedBy
| Signature | Return | Purpose |
|---|---|---|
fetchedBy(Scribe scribe) | MockEntry | Bake that Scribe's SELECT clause onto this entry as its contract |
Entries returned through MockEloquent pick up the contract automatically from the Scribe you passed to get(scribe). That is 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 the contract after the fact.
MockEntry card = MockEntry.of(BusinessCard__c.class)
.autoId(1)
.set('CompanyName__c', 'Acme')
.fetchedBy(RematchCompanyCardsHandler.scope());
Where this earns its keep is mainly batches. Records arriving at execute(bc, scope) never pass through IEloquent, so production is covered by the platform (they are real query results) while tests are not checked at all — you built those entries yourself. Pull the query construction into a @TestVisible method and the test can hand over exactly the same Scribe production uses.
See "Extract the Scribe and SELECT-omission detection works in tests too" in API Reference: Scribe for the full pattern.
Production
Entryneeds nofetchedBy. A real SOQL row already throws from the platform the moment you touch an unqueried field. This is a strictness tool the mock side alone requires.
Relations (building)
| Signature | Return | Purpose |
|---|---|---|
setParent(String parentIdFieldName, MockEntry parent) | MockEntry | Hang a parent record under the entry |
setChildren(String name, List<MockEntry> children) | MockEntry | Hang a child-record list under the entry |
For the first argument of setChildren, use the name you specified in Scribe via relationName if you did; otherwise use the object name (e.g. 'Opportunity' — singular, no __r).
⚠️ Keep the
Scribekey and thesetChildrenkey aligned.
MockEntry.getChildrenchecks against the child relation name theScriberegistered (the object name unlessrelationNamewas set). If they diverge, anApexEloquentExceptionis thrown. ProductionEntryresolves relationship names too, but in mocks you must call with the key you declared on theScribe. It never quietly returns an empty list, so a mismatched key fails the test on the spot.
Auto-Generated Id
| Signature | Return | Purpose |
|---|---|---|
autoId(Integer suffix) | MockEntry | Auto-generate an 18-character Id (numeric suffix) |
autoId(String suffix) | MockEntry | Auto-generate an 18-character Id (string suffix; supports placeholder expansion like {#}) |
Bulk Generation (times)
| Signature | Return | Purpose |
|---|---|---|
times(Integer count) | List<MockEntry> | Expand count records from the template. Replaces placeholders {#} {A} {a} with sequential values |
times(Integer count, Integer startAt) | List<MockEntry> | Specify the start of the sequence |
times(Integer count, Integer startAt, Integer interval) | List<MockEntry> | Specify the start and increment |
List<MockEntry> contacts = MockEntry.of(Contact.class)
.autoId('{#}')
.set('LastName', 'Contact-{#}')
.alias('con_{#}')
.times(3); // con_1, con_2, con_3
Nested multiplication is not supported: you cannot expand "2 parents × 4 children each" hierarchically by having MockEntry.of(Account.class).times(2) contain times(4) on the child side. Build the child side by enumerating individual entries inside setChildren.
alias (Retrieving Generated Ids)
| Signature | Return | Purpose |
|---|---|---|
alias(String name) | MockEntry | Attach a name to this entry |
getByAlias(String name) | MockEntry | Retrieve a MockEntry by alias via recursive search |
getAliasId(String name) | Id | Retrieve the auto-generated Id of an alias (handy for assertions) |
MockEntry oppEntry = MockEntry.of(Opportunity.class)
.alias('opp').autoId(1);
Id oppId = oppEntry.getAliasId('opp');
// In the test:
Opportunity updated = (Opportunity) mock.upsertedRecordsAt(MyUsecase.LBL_UPDATE)[0];
Assert.areEqual(oppId, updated.Id);
Disabling Detection (Two Layers)
MockEntry has detection in two independent layers, each with its own escape hatch. Neither should be used as a rule — both safety nets exist to close off false positives.
| Signature | Return | What It Disables |
|---|---|---|
withoutFieldValidation() | MockEntry | Scribe FieldStructure check (permits get(...) access to fields not SELECTed in Scribe) |
withoutSObjectFieldValidation() | MockEntry | SObject field-name check (permits non-existent field names in set / add / setParent / addParent) |
// Example: a rare case where you want to store data under a name that isn't an SObject field
MockEntry.of(Account.class)
.withoutSObjectFieldValidation()
.set('Synthetic__c', 'value'); // not an Account field, but permitted
These two are separate flags. Relaxing the Scribe check via withoutFieldValidation() keeps the SObject typo detector intact (a clear set('Naame', ...) typo still throws). The reverse holds as well. Each escape hatch disables only its own responsibility — the other safety net stays on.
Read Next
- Data Access, DML, IEntry, and Mock: the usage guide
- Parent Fields, Child Subqueries, and Many-to-Many: typical relation-operation examples
- API Reference: IEloquent / Eloquent / MockEloquent: the side that returns
IEntry - API Reference: Scribe: the side that builds the source data for SELECT-omission detection
- ApexEloquent Guide: back to the guide index