# 親項目・子サブクエリ・多対多

このドキュメントは、ApexEloquent でリレーションを扱う方法をまとめます。`Scribe` でのクエリ組み立ては [Scribe でクエリを組み立てる](/ja/apex-stem/docs/apex-eloquent-scribe-guide)、`IEloquent` / `IEntry` の基本は [データ取得と DML、IEntry、Mock](/ja/apex-stem/docs/apex-eloquent-data-access) を参照してください。

## 親項目を取得する

「商談から親取引先の業種を取りたい」のように、親オブジェクトのフィールドを SELECT に含めるには `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');
}
```

- `Scribe.asParent('AccountId')` で親リレーション用の Scribe を作り、`.field(...)` で取り込む親項目を指定
- それを `parentField(...)` に渡すと、SOQL の SELECT 句に `Account.Name` のように展開される
- 取得後は `IEntry.getParent('AccountId')` で親 `IEntry` を取り出す

### parentField で取らないと例外

`Scribe` で `parentField` していない親項目に `IEntry.getParent('AccountId')` でアクセスすると、テスト時 (`MockEntry` 経由) に例外が出ます。これは「親項目の SELECT 漏れ」を本番ではなくテストで検出するための仕組みです。

## 子サブクエリを取得する

「取引先からその子商談一覧を取りたい」のように、子レコードをサブクエリで取るには `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) {
    // ...
  }
}
```

- `Scribe.asChild(Opportunity.class)` で子サブクエリ用の `Scribe` を作る
- それを `withChildren(...)` に渡すと、SOQL の SELECT 句にサブクエリとして展開される
- 取得後は `IEntry.getChildren('Opportunity')` で子 `IEntry` のリストを取り出す

本番の `Entry` は、`getChildren` の引数を **オブジェクト名と relationship 名のどちらでも解決します** (`'Opportunity'` / `'Opportunities'`)。まずオブジェクト名として探し、見つからなければ relationship 名として解決する順です。

ただし **`MockEntry` は `Scribe` に宣言したキーで照合する** ので、テストを通したいなら `Scribe` と揃えた名前で呼んでください (後述の「子レコードのモック」を参照)。**テストが通る書き方は本番でも通ります**が、逆は保証されません。

### 並列に複数の子サブクエリを取る

同じ親に対して **複数の子オブジェクトを並列に** 取得することもできます。`withChildren` を 2 回続ければ、それぞれが独立した子サブクエリになります。

```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'
```

### ネストした子サブクエリ (子の子) を取る

`Scribe.asChild(...)` の中でさらに `withChildren(...)` を呼ぶと、子の子も含めたネストしたサブクエリになります (最大 4 レベルまで)。

```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'
```

取得側は `getChildren` をネストして辿ります。

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

### 子リレーションが曖昧な時は relationName を明示

同じオブジェクトを参照する Lookup フィールドが複数ある場合 (`Opportunity.AccountId` と `Opportunity.CustomAccount__c` のように、両方が `Account` を指すケース)、子サブクエリは **どのリレーションで戻るかが曖昧** になり、`Scribe` のままだとエラーになります。

このときは `relationName(...)` で明示します。

```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

// 取得側もこのリレーション名で
accountEntry.getChildren('CustomOpportunities__r');
```

## 親条件で絞り込む

「子オブジェクトを、親オブジェクトの条件で絞り込みたい」場合は `parentCondition` を使います。

```apex
// 親 Opportunity の Name が Test% で始まる OpportunityLineItem を取得
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%'
```

`Scribe.asParent('OpportunityId')` に WHERE 系メソッドを連ねて、それを `parentCondition` で組み込みます。

`parentCondition` は **SELECT には親項目を含めない、ただ条件としてだけ使う** 形になります。親項目も SELECT したい場合は `parentField` も併用してください。

### 親条件を OR でつなぐ

`parentCondition` 内の `Scribe.asParent(...)` でも `orCondition()` が使えるので、「親の Name または親の Type が一致する」のような条件が書けます。

```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')
```

条件を 2 つ以上持つ `parentCondition` は、**ひとつの構造単位として括弧で囲まれます**。囲まないと `A AND B OR C` という「1 階層で AND と OR が混ざった」SOQL になり、SOQL 側が `unexpected token: OR` で拒否するためです。

> ⚠️ **v3.5.0 未満では括弧が付かず、実行時に落ちます。** `toSoql()` は成功するのに実クエリだけが失敗するため気づきにくいバグでした。複数条件の `parentCondition` を他の条件と組み合わせるなら v3.5.0 以上を使ってください。

`parentField` と `parentCondition` は両立し、それぞれ SELECT 句と WHERE 句に独立して反映されます。

## 多対多 (Junction Object)

Salesforce で多対多関係を表現する Junction Object を経由した取得には `asThrough` + `through` を使います。

標準オブジェクトでわかりやすい例として、注文 (`Order`) と商品 (`Product2`) の関係を考えます。両者の間には注文明細 (`OrderItem`) が Junction として挟まり、`OrderItem.Product2Id` で `Product2` を、`OrderItem.OrderId` で `Order` を参照します。

「ある注文が扱う商品を取りたい」場合:

```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
```

ポイント:
- `Scribe.asThrough(OrderItem.class, 'Product2Id')` で「`OrderItem` 経由で、`Product2Id` の先 (= `Product2`) を取りに行く」と宣言
- `.field('Name')` / `.field('ProductCode')` のように、**通過先 (`Product2`) のフィールド名で書く**。生成 SOQL では自動的に `Product2.Name` / `Product2.ProductCode` に展開される
- `.whereEqual('IsActive', true)` のような WHERE も、通過先 (`Product2`) を基準に書く。SOQL では `WHERE Product2.IsActive = true` になる

取得側は `IEntry.getThrough(junctionName, relatedKey)` で取り出します。第 1 引数は `getChildren` と同じく、**Junction のオブジェクト名と relationship 名のどちらでも解決されます**。

```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');
  }
}
```

Junction 経由でも子サブクエリと同じく、関連が曖昧な場合は `relationName(...)` で明示できます。同じ親に複数の lookup がある等のケースで使います。

## MockEntry で親子をモックする

テストデータとして親子関係を組み立てるには、`MockEntry.setParent` と `MockEntry.setChildren` を使います。

### 親レコードのモック

```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')
  );

// テストコード内で取得側のコードに渡せば、getParent でアクセスできる
IEntry accountEntry = oppEntry.getParent('AccountId');
Assert.areEqual('Technology', (String) accountEntry.get('Industry'));
```

`setParent('AccountId', ...)` で「商談の `AccountId` 経由の親」として取引先 `MockEntry` をぶら下げます。

### 子レコードのモック

```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)
  );

// テストでは getChildren でアクセス
List<IEntry> oppEntries = accountEntry.getChildren('Opportunity');
Assert.areEqual(3, oppEntries.size());
```

`setChildren` の第 1 引数は、`Scribe` 側で `relationName` を指定したならその名前、指定していないなら **オブジェクト名** (`'Opportunity'` のようにそのまま、複数形変換も `__r` も付けない) で揃えます。

> ⚠️ **`Scribe` の登録キーと `setChildren` の key は揃えてください。**
> `MockEntry.getChildren` は、`Scribe` が登録した子リレーション名 (`relationName` 未指定ならオブジェクト名) と照合します。ズレていると **`ApexEloquentException` が投げられます**。
>
> ```
> The specified child Object Name `Opportunities` is not set in Scribe. parent object name: Account
> ```
>
> つまり、本番の `Entry` は relationship 名でも解決しますが、**モックでは `Scribe` に宣言したキーで呼ぶ必要があります**。黙って空リストが返ることはないので、キーがズレていればテストがその場で落ちます。

### 子 MockEntry は setChildren の引数内でインライン定義する

子の `MockEntry` を事前に変数化すると、「この変数が後でどこか別の場所で使われるのか?」と読み手が予測しないといけなくなります。`getAliasId(...)` などで再利用する明確な理由がない限り、`setChildren` の引数の中で **インラインで書き下す** のが読みやすくなります。

```apex
// ✅ インライン (構造が視覚的に見える)
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')
  });
```

## 次に読む

- [Scribe でクエリを組み立てる](/ja/apex-stem/docs/apex-eloquent-scribe-guide): クエリビルダーの全体像
- [データ取得と DML、IEntry、Mock](/ja/apex-stem/docs/apex-eloquent-data-access): `IEloquent` と `MockEloquent` の使い方
- [ApexEloquent ガイド](/ja/apex-stem/docs/apex-eloquent-guide): ApexEloquent ガイドの目次に戻る
