# 依存解決の仕組み: トポロジカルソート + alias 解決

> **この記事の対象読者**: ApexBlueprint の **内部実装** が気になる方。「シンプルな API の裏側で何が起きているか」を、フェーズ単位の擬似コードと実装ファイル (`SOrchestrator.cls` / `SBlueprintAnalyzer.cls` / `SBlueprintRealizer.cls`) の参照付きで追いたい開発者向け。設計判断の背景や哲学に焦点を当てた姉妹記事は [Declarative Data Specification: なぜ blueprint 形式か](/ja/apex-stem/docs/declarative-data-specification) を参照してください。

ApexBlueprint の API は `of` / `set` / `template` / `alias` / `use` / `times` / `withChildren` / `parentIdField` の **わずか 8 種類** です。しかしこれだけで、親子のネスト、量産、兄弟参照、「自分の真の親」の階層的解決、複数 lookup の曖昧解消といった、結合テストデータでよく出会う困難をひと通り扱えます。

このギャップは、SOrchestrator の `.create()` 内部で **複数の独立した問題が一手に解かれている** ことから生まれます。このページでは、利用者の視点からは見えない裏側で何が起きているのかを、フェーズごとに掘り下げます。

## 利用者の視点 vs 内部の処理

利用者の視点から見ると、`.create()` は「**追加した全ての blueprint を、正しい順番でデータベースに挿入する**」という 1 つの動作に見えます。

しかし内部ではこれを実現するために、4 つの独立した問題が解かれています:

| 問題 | 解決の中心 |
|---|---|
| 1. どんな順序で insert すれば lookup が解決するか | 依存解析 + トポロジカルソート |
| 2. 「親 1 の下の子 1」と「親 2 の下の子 1」をどう識別するか | alias 解決 + 自動 alias 払い出し |
| 3. `.times(...)` で量産されたレコードを階層的にどう関連付けるか | 親ごとの繰り返し + 親 Id 自動転記 |
| 4. `{P0}` / `{P1}` で「自分の真の親」をどう特定するか | 階層スタックの追跡 + 親参照解決 |

これらは普通バラバラに頭を悩ます類の問題ですが、ApexBlueprint はこれらを **同じパイプラインの中で連続的に解いて** います。

## 内部処理のフェーズ

`.create()` を呼んだとき、内部では大きく分けて次の流れが進行します。

### フェーズ 1: 設計図の収集

`SOrchestrator.start().add(...).add(...)` で渡された `SBlueprint` 群は、まず **追加順のまま** SOrchestrator 内部のリストに保持されます。この時点では何の検証も実行もされません。

`withChildren` でネストされた子の blueprint は、親 blueprint の中で **木構造** として保持されています。つまりルート blueprint をたどれば、そこから下の子・孫・ひ孫がすべて取り出せる状態です。

### フェーズ 2: 依存グラフの構築

`.create()` が呼ばれたタイミングで、SOrchestrator はまず **依存グラフを構築** します。各 blueprint をノードとして、次の関係を有向辺として登録します:

- **`withChildren` の親子関係**: 親 → 子 への辺 (子は親の Id に依存する)
- **`.use(alias, ...)` の兄弟参照**: alias 元の blueprint → 自 blueprint への辺 (自分は alias 元の値に依存する)
- **`.after(alias)` の順序指定**: 同じく alias 元 → 自 blueprint への辺。ただし **値を運ばない辺** で、順序の制約だけを表す
- **`{P0}` / `{P1}` 等の親参照**: 該当する祖先 → 自 blueprint への辺 (構築段階で対応する祖先が特定される)

なお、**alias の重複** や **存在しない alias の参照** の検出は、ここで構築された依存情報をもとに後段の フェーズ 5 (realize 時) に **遅延検出** されます。利用者から見れば「`.create()` 時に失敗する」種類のエラーに変わりはありませんが、実装上は「グラフ構築」と「整合性検証」が分離されている点を補足しておきます。

### フェーズ 3: トポロジカルソート

依存グラフが完成したら、SOrchestrator は **トポロジカルソート** を実行して、依存される側 (親) から先に来るように blueprint の順序を並べ替えます。

- 依存関係に **循環** が見つかると、「`Circular or invalid reference detected`」で失敗
- 一度ソートに成功すれば、後段の挿入処理は **insert 順を考えなくてよい** ことが保証される

利用者が `.add(...)` を「読みやすい順」で並べて書けるのは、ここで順序がリセットされるためです。

### フェーズ 4: alias 解決と自動 alias 払い出し

ソート済みの blueprint を順番に処理する過程で、各 blueprint には alias が割り当てられます。

- `.alias(...)` で明示された alias は **そのまま使われる** (`{#}` プレースホルダは展開後に確定)
- `.alias(...)` を呼ばなかった blueprint には、**自動 alias** が割り当てられる

自動 alias のフォーマットは内部的に `__{SObjectName}_{グローバルカウンター}_{#}__` の形 (例: `__Account_0_1__` / `__Contact_1_2__`)。ここでの中段の数値は **階層深度ではなく blueprint 全体で振られるグローバルカウンター** です (テストの assertion で見える `__Contact_1_1__` の `1` は深度ではなく、解析の登場順で振られた序列)。階層構造はこの後説明する **親プレフィックス** の方で表現されます。

`withChildren` でネストした blueprint には、さらに親の alias がプレフィックスされて `__Account_0_1____Contact_1_1__` のような複合 alias が払われます。これにより「親 1 の下の子 1」と「親 2 の下の子 1」が **別レコードとして識別可能な名前空間** を持てるようになります。

### フェーズ 5: 階層的な realize と親 Id 転記

ここからが Realizer の出番です。ソート済みの blueprint を順番に **realize** (= SObject インスタンスに変換) していきます。

ネスト + `.times(...)` の組み合わせがあるとき、realize はこう振る舞います:

```
親 blueprint を times(N) 回ループ
  各親インスタンスごとに:
    親を SObject 化
    子の blueprint を times(M) 回ループ
      各子インスタンスごとに:
        子を SObject 化
        子の lookup フィールドに親の Id をコピー
        ※ Id はこの時点ではまだ仮の値 (insert 前)
        孫があれば再帰的に同じ処理
```

ここで重要なのは、**親の各インスタンスごとに子の完全セットが再生成される** という性質です。これが「Multiplication: 上位階層の times が下位に伝播する」仕様の源泉になっています ([親子・量産・参照のパターン](/ja/apex-stem/docs/apex-blueprint-relations-and-bulk) 参照)。

#### `{P0}` / `{P1}` の解決はどこで行われるか

`{Pn}` の解決は、まさにこの「親ごとに子を realize する」ループの中で行われます。Realizer は再帰の過程で **親位置マップ** (`parentPositionToAlias` という Map) を動的に組み立てており、子 blueprint の `.use('{P1}', 'LastName', 'Subject')` のような宣言は、「いまこの瞬間に親位置マップ上で階層 1 (= 自分にとっての真の親) を担当している blueprint の alias」として解決されます。

つまり `{Pn}` は **静的な alias 文字列ではなく、動的に組み立てられた親位置マップへの参照** です。階層が深くなった分だけマップにエントリが追加されていくため、`{P0}` / `{P1}` / `{P2}` の数値は **ルートからの絶対深度** として安定して指定できます。

alias で `{P0}_child_{#}` のように親 alias を埋め込んで同じことをする方法もありますが (これも実装としてサポートされている)、利用者が `.use(...)` 側で alias 文字列を頭で組み立てる必要が出るため、`{Pn}` の方が **構造的に書きやすい** 設計になっています。

> ここで説明した realize は、後述の フェーズ 6 と組み合わさって **1 レイヤーずつ** 実行される点に注意してください。「全 blueprint を一気に realize してから一括 insert」ではありません (詳細は次の フェーズ 6 で)。

### フェーズ 6: レイヤーごとの一括 DML 挿入 (フェーズ 5 と交互ループ)

実装上、フェーズ 5 (realize) と フェーズ 6 (DML insert) は **一気通貫ではなく、フェーズ 3 で算出された「レイヤー」単位で交互に繰り返されます**。ここでの「レイヤー」とは、依存解析の結果 **同じ深さに位置する blueprint の集合** のことで、ルートに近い blueprint がレイヤー 0、その子がレイヤー 1、という形で割り当てられます (`SOrchestrator.BuildLayers`)。

`.create()` 実行時の擬似コードは次のようになります:

```
for each layer from 0 to maxLayer:
  // フェーズ 5: このレイヤー内の全 blueprint を realize
  //   上位レイヤーが既に insert 済みなので、 親 Id が確定した状態で .use(parentAlias, 'Id', ...) を解決できる
  layerSObjects = realize all blueprints in this layer

  // フェーズ 6: このレイヤーの SObject を一括 insert
  dmlOperator.doInsert(layerSObjects)

  // マスターマップ aliasToSObject にこのレイヤーの結果を格納
  // 次のレイヤーの realize 時に参照される
  aliasToSObject.putAll(thisLayerResults)
```

なぜレイヤー間で同期するかというと、**親レイヤーが insert されてはじめて Id が確定し、次レイヤーの子が `.use(parentAlias, 'Id', 'AccountId')` で参照する本物の Id が手に入る** からです。全てを先に realize してしまうと、親の Id が仮置きのまま子に転記されてしまい、insert 後に親子のリレーションが破綻します。

このレイヤー間の同期があるからこそ、利用者は「`.use(...)` で親 Id を指定するだけで、実 DML 挿入後の本物の Id が子に転記されている」という挙動を素直に享受できています。

#### IDmlOperator の差し替え位置

各レイヤー末尾の `dmlOperator.doInsert(layerSObjects)` の `dmlOperator` が、本番 / Mock の差し替え対象です:

- 本番は `DmlOperator` (実 `insert` を実行)
- テストでは `MockDmlOperator` (実 DML を発火せず、仮 Id を払い出すのみ)

ApexBlueprint 自身のテスト (`SOrchestratorTest`) で `SOrchestrator.start(new MockDmlOperator())` を使うのは、ここを差し替えて **実 DML を発火させずに挙動を検証** するためです。

#### alias 重複検出のタイミング

実装上、alias 重複は次の 2 タイミングで検出されます:

- **同一レイヤー内**: `realizeLayer` で `layerAliasToSObject` に詰める段階
- **異なるレイヤー間**: insert 後にマスターマップ `aliasToSObject` へ詰める段階

いずれも実行時に `Duplicate alias detected` 例外が投げられます。これは フェーズ 2 で書いた「alias 重複は遅延検出される」の具体的な発生位置です。

## なぜこれらが一手に解けているか

ApexBlueprint の内部処理を振り返ると、「依存グラフ」「トポロジカルソート」「alias 解決」「親ごとの再帰 realize」「親参照解決」「一括 DML」という独立した問題が、同じパイプラインの中で連続的に解かれています。

これらが利用者から見ると「依存を宣言したら勝手に動く」という 1 つの操作に縮退している理由は、ApexBlueprint が **「データの最終状態の宣言」という 1 つの抽象** にすべての操作を寄せているからです。利用者は「最終的にどんなレコード群があってほしいか」だけを書き、「そこに到達するための機械的な手続き」はフレームワークがまとめて引き受けています。

設計の妙としては、次の 2 点が際立っています:

1. **API の表面は 11 メソッドに収まり、学習コストが線形にしか増えない**。機能追加のたびにメソッド数が指数的に増えるタイプの API ではない
2. **裏側の各問題に対する解 (トポロジカルソート / 自動 alias / `{Pn}` の階層スタック解決) がそれぞれ独立して交換可能** な構造になっており、内部実装の改善余地が温存されている

「シンプルな表面と、強力な内部」という ApexBlueprint の性格は、こうした分離された解の積み上げによって成立しています。

### 実例: v2.0.0 の 2 機能は、新しい機構を足していない

上記 1 の主張は抽象論に見えますが、v2.0.0 の追加がそのまま実例になっています。**`after` と `sharedWith` は、どちらも依存解決に新しい仕組みを一切足していません。**

| 追加 API | 内部でやっていること |
|---|---|
| `.after(alias)` | `fromField` / `toField` を持たない依存を、`.use()` と**同じ依存リスト**に積む。トポロジカルソートから見れば「値を運ばない辺」が 1 本増えただけ |
| `.sharedWith(user, level)` | `__Share` の兄弟 blueprint を組み立て、`.use(自分のalias, 'Id', 'ParentId')` で自分に繋ぐ。**普通の子ノードが 1 つ増えただけ** |

`sharedWith` が「親より必ず 1 レイヤー後に insert される」のも、共有のための特別な順序制御があるからではなく、**`use()` の辺が張られた結果としてフェーズ 3 のソートが自然にそう並べる**からです。同じ理由で、`times` による量産にも `{Pn}` の解決にも自動的に追随します (共有のために量産の仕組みを書き直した箇所は存在しません)。

新機能が「フェーズ 2 でどんな辺を張るか」の表現に落ちる限り、フェーズ 3 以降には手を入れずに済みます。**API 表面が線形にしか増えないのは、この構造の帰結です。**

## 関連ドキュメント

- [Declarative Data Specification: なぜ blueprint 形式か](/ja/apex-stem/docs/declarative-data-specification): なぜこの設計に至ったかの哲学を扱う姉妹 Deep Dive
- [親子・量産・参照のパターン](/ja/apex-stem/docs/apex-blueprint-relations-and-bulk): 利用者から見える表面の API パターン集
- [API リファレンス: SOrchestrator](/ja/apex-stem/docs/apex-blueprint-api-sorchestrator): start / add / create / getByAlias と各例外
- [API リファレンス: SBlueprint](/ja/apex-stem/docs/apex-blueprint-api-sblueprint): メソッドチェーンの全 API
- [ApexBlueprint ガイドへ戻る](/ja/apex-stem/docs/apex-blueprint-guide)
