# Parent Fields, Child Subqueries, and Many-to-Many

This document covers how to work with relations in ApexEloquent. For query assembly via `Scribe`, see [Building Queries with Scribe](/apex-stem/docs/apex-eloquent-scribe-guide). For the basics of `IEloquent` / `IEntry`, see [Data Access, DML, IEntry, and Mock](/apex-stem/docs/apex-eloquent-data-access).

## Retrieving Parent Fields

To pull a parent object's field into the SELECT — like "from an Opportunity, fetch the parent Account's industry" — use `parentField`.

```apex
Scribe oppScribe = Scribe.of(Opportunity.class)
  .field('Id')
  .field('Name')
  .parentField(Scribe.asParent('AccountId').field('Name').field('Industry'))
  .whereEqual('StageName', 'Closed Won');
// → SELECT id, name, Account.name, Account.industry FROM Opportunity WHERE StageName = 'Closed Won'

List<IEntry> oppEntries = this.fetchEloquent.get(oppScribe);

for (IEntry oppEntry : oppEntries) {
  IEntry accountEntry = oppEntry.getParent('AccountId');
  String accountName = accountEntry.getName();
  String industry = (String) accountEntry.get('Industry');
}
```

- Create a parent-relation Scribe with `Scribe.asParent('AccountId')` and specify the parent fields via `.field(...)`
- Pass it to `parentField(...)`, and the SOQL SELECT clause expands to `Account.Name` and friends
- After fetching, retrieve the parent `IEntry` via `IEntry.getParent('AccountId')`

### Accessing a Non-parentField'd Parent Throws

Calling `IEntry.getParent('AccountId')` on a parent that wasn't declared with `parentField` in `Scribe` throws at test time (via `MockEntry`). This is the mechanism that surfaces "parent-field SELECT omissions" in tests rather than in production.

## Retrieving Child Subqueries

To pull child records via a subquery — like "from an Account, fetch its list of child Opportunities" — use `withChildren`.

```apex
Scribe accountScribe = Scribe.of(Account.class)
  .field('Id')
  .field('Name')
  .withChildren(
    Scribe.asChild(Opportunity.class)
      .field('Id')
      .field('Name')
      .field('StageName')
  )
  .whereEqual('Type', 'Customer');
// → SELECT id, name, (SELECT id, name, stagename FROM Opportunities) FROM Account WHERE Type = 'Customer'

List<IEntry> accountEntries = this.fetchEloquent.get(accountScribe);

for (IEntry accountEntry : accountEntries) {
  List<IEntry> oppEntries = accountEntry.getChildren('Opportunity');
  for (IEntry oppEntry : oppEntries) {
    // ...
  }
}
```

- Create a child-subquery `Scribe` with `Scribe.asChild(Opportunity.class)`
- Pass it to `withChildren(...)`, and the SOQL SELECT clause expands as a subquery
- After fetching, retrieve the child `IEntry` list via `IEntry.getChildren('Opportunity')`

In production, `Entry` resolves the `getChildren` argument as **either an object name or a relationship name** (`'Opportunity'` / `'Opportunities'`) — object name first, falling back to relationship name.

That said, **`MockEntry` checks against the key you declared on the `Scribe`**, so call it with the name you used there if you want the test to pass (see "Mocking child records" below). **Anything that passes in tests will pass in production**; the reverse is not guaranteed.

### Fetching Multiple Child Subqueries in Parallel

Multiple child objects can be fetched **in parallel** under the same parent. Chain `withChildren` twice and each becomes an independent child subquery.

```apex
Scribe scribe = Scribe.of(Account.class)
  .field('Id')
  .withChildren(
    Scribe.asChild(Opportunity.class).field('Id').whereNotNull('Name')
  )
  .withChildren(
    Scribe.asChild(Contact.class).field('Id').whereNotNull('Email')
  )
  .whereEqual('Name', 'Test Account');
// → SELECT id, (SELECT id FROM Opportunities WHERE Name != NULL), (SELECT id FROM Contacts WHERE Email != NULL) FROM Account WHERE Name = 'Test Account'
```

### Nested Child Subqueries (Children of Children)

Calling `withChildren(...)` inside a `Scribe.asChild(...)` gives you nested subqueries that include grandchildren (up to four levels deep).

```apex
Scribe scribe = Scribe.of(Account.class)
  .field('Id')
  .withChildren(
    Scribe.asChild(Contact.class)
      .field('Id')
      .whereNotNull('Name')
      .withChildren(
        Scribe.asChild(Opportunity.class).field('Id').whereNotNull('Email')
      )
  )
  .whereEqual('Name', 'Test Account');
// → SELECT id, (SELECT id, (SELECT id FROM Opportunities WHERE Email != NULL) FROM Contacts WHERE Name != NULL) FROM Account WHERE Name = 'Test Account'
```

On the consumer side, walk through nested `getChildren` calls.

```apex
for (IEntry accountEntry : accountEntries) {
  for (IEntry contactEntry : accountEntry.getChildren('Contact')) {
    for (IEntry oppEntry : contactEntry.getChildren('Opportunity')) {
      // ...
    }
  }
}
```

### Make relationName Explicit When the Child Relation Is Ambiguous

When multiple lookup fields reference the same object (e.g. `Opportunity.AccountId` and `Opportunity.CustomAccount__c` both pointing at `Account`), the child subquery becomes **ambiguous about which relation to return**, and `Scribe` errors out as-is.

In that case, use `relationName(...)` to disambiguate.

```apex
Scribe accountScribe = Scribe.of(Account.class)
  .field('Id')
  .withChildren(
    Scribe.asChild(Opportunity.class)
      .relationName('CustomOpportunities__r')  // ← Custom Relationship Name
      .field('Id')
      .field('Name')
  );
// → SELECT id, (SELECT id, name FROM CustomOpportunities__r) FROM Account

// Use the same relation name on the consumer side
accountEntry.getChildren('CustomOpportunities__r');
```

## Filtering by Parent Conditions

To filter a child object based on the **parent's** condition, use `parentCondition`.

```apex
// Fetch OpportunityLineItem where the parent Opportunity's Name starts with "Test%"
Scribe scribe = Scribe.of(OpportunityLineItem.class)
  .field('Id')
  .field('Quantity')
  .parentCondition(
    Scribe.asParent('OpportunityId').whereLike('Name', 'Test%')
  );
// → SELECT id, quantity FROM OpportunityLineItem WHERE Opportunity.Name LIKE 'Test%'
```

Chain WHERE-family methods onto `Scribe.asParent('OpportunityId')` and embed it via `parentCondition`.

`parentCondition` **does not include parent fields in the SELECT — it uses the parent only as a condition**. If you also want parent fields in the SELECT, combine with `parentField`.

### Joining Parent Conditions with OR

`orCondition()` is usable inside the `Scribe.asParent(...)` of a `parentCondition`, so conditions like "the parent's Name or the parent's Type matches" work fine.

```apex
Scribe scribe = Scribe.of(Opportunity.class)
  .field('Id')
  .parentField(Scribe.asParent('AccountId').field('Name').field('Id'))
  .whereEqual('Name', 'Test Opportunity')
  .parentCondition(
    Scribe.asParent('AccountId')
      .whereEqual('Name', 'Test Account')
      .orCondition()
      .whereEqual('Type', 'Test Type')
  );
// → SELECT id, Account.name, Account.id FROM Opportunity WHERE Name = 'Test Opportunity' AND (Account.Name = 'Test Account' OR Account.Type = 'Test Type')
```

A `parentCondition` holding two or more conditions is **wrapped in parentheses as a single structural unit**. Without them the SOQL would read `A AND B OR C` — AND and OR mixed at one nesting level — which SOQL rejects with `unexpected token: OR`.

> ⚠️ **Before v3.5.0 the parentheses were missing and the query failed at run time.** `toSoql()` succeeded while only the real query died, which made it hard to spot. Use v3.5.0 or later if you combine a multi-condition `parentCondition` with any other condition.

`parentField` and `parentCondition` coexist and reflect independently in the SELECT and WHERE clauses.

## Many-to-Many (Junction Object)

For retrieval through a junction object — Salesforce's way of expressing many-to-many — use `asThrough` + `through`.

A clear standard-object example: the relationship between an Order (`Order`) and a Product (`Product2`). Between them sits the order item (`OrderItem`) as a junction, referencing `Product2` via `OrderItem.Product2Id` and `Order` via `OrderItem.OrderId`.

"Fetch the products an order handles":

```apex
Scribe scribe = Scribe.of(Order.class)
  .field('Id')
  .through(
    Scribe.asThrough(OrderItem.class, 'Product2Id')
      .field('Name')
      .field('ProductCode')
      .whereEqual('IsActive', true)
  );

// → SELECT id, (SELECT product2id, Product2.name, Product2.productcode FROM OrderItems WHERE Product2.IsActive = true) FROM Order
```

Points:
- `Scribe.asThrough(OrderItem.class, 'Product2Id')` declares "through `OrderItem`, fetch what `Product2Id` points to (= `Product2`)"
- `.field('Name')` / `.field('ProductCode')` — **write field names of the destination (`Product2`)**. The generated SOQL auto-expands them to `Product2.Name` / `Product2.ProductCode`
- `.whereEqual('IsActive', true)` etc. are also based on the destination (`Product2`). The SOQL becomes `WHERE Product2.IsActive = true`

On the consumer side, retrieve via `IEntry.getThrough(junctionName, relatedKey)`. As with `getChildren`, the first argument resolves as **either the junction's object name or its relationship name**.

```apex
List<IEntry> orderEntries = this.fetchEloquent.get(scribe);
for (IEntry orderEntry : orderEntries) {
  List<IEntry> productEntries = orderEntry.getThrough('OrderItem', 'Product2Id');
  for (IEntry productEntry : productEntries) {
    String name = (String) productEntry.get('Name');
    String code = (String) productEntry.get('ProductCode');
  }
}
```

Even via a junction, you can use `relationName(...)` to disambiguate when the relation is ambiguous — same idea as with child subqueries. Useful when the same parent has multiple lookups.

## Mocking Parent-Child with MockEntry

To assemble parent-child relationships as test data, use `MockEntry.setParent` and `MockEntry.setChildren`.

### Mocking the Parent

```apex
MockEntry oppEntry = MockEntry.of(Opportunity.class)
  .alias('opp').autoId(1)
  .set('Name', 'Test Opp')
  .setParent('AccountId',
    MockEntry.of(Account.class)
      .set('Name', 'Parent Account')
      .set('Industry', 'Technology')
  );

// In the test, hand it off to the consumer code and access via getParent
IEntry accountEntry = oppEntry.getParent('AccountId');
Assert.areEqual('Technology', (String) accountEntry.get('Industry'));
```

`setParent('AccountId', ...)` hangs an Account `MockEntry` as "the parent via the Opportunity's `AccountId`".

### Mocking Children

```apex
MockEntry accountEntry = MockEntry.of(Account.class)
  .alias('acc').autoId(1)
  .set('Name', 'Acc Co.')
  .setChildren('Opportunity',
    MockEntry.of(Opportunity.class)
      .autoId('{#}')
      .set('Name', 'Opp-{#}')
      .set('StageName', 'Prospecting')
      .times(3)
  );

// In the test, access via getChildren
List<IEntry> oppEntries = accountEntry.getChildren('Opportunity');
Assert.areEqual(3, oppEntries.size());
```

For the first argument of `setChildren`, use the same name you specified in `Scribe` with `relationName` if you did; otherwise use the **object name** as-is (`'Opportunity'` — no pluralization, no `__r`).

> ⚠️ **Keep the `Scribe` key and the `setChildren` key aligned.**
> `MockEntry.getChildren` checks against the child relation name the `Scribe` registered (the object name unless `relationName` was set). Diverge and **an `ApexEloquentException` is thrown**.
>
> ```
> The specified child Object Name `Opportunities` is not set in Scribe. parent object name: Account
> ```
>
> So while production `Entry` also resolves relationship names, **in mocks you must call with the key declared on the `Scribe`**. It never quietly returns an empty list — a mismatched key fails the test right there.

### Define Child MockEntry Inline Inside the setChildren Argument

If you extract the child `MockEntry` into a variable, the reader has to anticipate "is this variable used somewhere else later?". Unless there's a clear reason to reuse it via `getAliasId(...)` or similar, **inline the definition inside the `setChildren` argument** for better readability.

```apex
// Good: inline (the structure is visually obvious)
MockEntry accountEntry = MockEntry.of(Account.class).alias('acc').autoId(1)
  .set('Name', 'Acc Co.')
  .setChildren('Opportunity', new List<MockEntry>{
    MockEntry.of(Opportunity.class).autoId(1).set('Name', 'Opp A'),
    MockEntry.of(Opportunity.class).autoId(2).set('Name', 'Opp B')
  });
```

## Read Next

- [Building Queries with Scribe](/apex-stem/docs/apex-eloquent-scribe-guide): the full picture of the query builder
- [Data Access, DML, IEntry, and Mock](/apex-stem/docs/apex-eloquent-data-access): how to use `IEloquent` and `MockEloquent`
- [ApexEloquent Guide](/apex-stem/docs/apex-eloquent-guide): back to the ApexEloquent Guide index
