SOrchestrator で依存解決と実 DML 挿入

Apex Stem ドキュメント
Apex StemApexBlueprintSOrchestratorTest DataDML
SBlueprint で組み立てた設計図を実レコードに変換する SOrchestrator の 4 メソッド (start / add / create / getByAlias) と、トポロジカルソート / 循環依存 / alias 重複などのハマりどころを解説します。

SBlueprint で組み立てた設計図を 実レコードに変換するエンジンSOrchestrator です。親 → 子 の挿入順、親 Id を子へ転記する処理、alias で参照した値の解決、これらをすべて自動でやってくれます。

このページでは、SOrchestrator の 4 つのメソッド (start / add / create / getByAlias) を順に解説し、最後に実運用でハマりやすいポイントをまとめます。

SOrchestrator.start(): builder の初期化

すべての操作はこの静的メソッドから始まります。戻り値は空の SOrchestrator インスタンスで、そこに blueprint を .add(...) して、最後に .create() で実行する、という流れになります。

シグネチャ: SOrchestrator.start()

APEX
SOrchestrator orchestrator = SOrchestrator.start();

.add(blueprint): blueprint をキューに登録する

.add(...) は 1 つの SBlueprint を SOrchestrator のキューに登録します。

シグネチャ: .add(SBlueprint blueprint)

APEX
SOrchestrator.start()
    .add(
        SBlueprint.of(Account.class)
            .template(Blueprints.accBasic())
            .alias('parentAccount')
    )
    .add(
        SBlueprint.of(Opportunity.class)
            .set('Name', 'Test Opportunity')
            .set('StageName', 'Prospecting')
            .set('CloseDate', Date.today().addDays(30))
            .use('parentAccount', 'Id', 'AccountId')
            .alias('targetOpp')
    );

振る舞いの要点

  • 追加順は無視される。SOrchestrator が内部で依存関係を解析し、トポロジカルソートで insert 順を再決定する
  • 子を先に書いて親を後に書いても問題ない。「読みやすい順」で書ける
  • 1 つの SOrchestrator に複数の .add(...) を連続して呼べる
  • 親子関係を 1 つの blueprint 内で表現する場合 (withChildren) は、親側だけを .add(...) すれば子も一緒に登録される (詳細は 親子・量産・参照のパターン)

.create(): 依存解決して DML 挿入する

.create() は、登録された blueprint 群を解析し、正しい順序で DML insert を実行 します。この呼び出しが終わった時点で、全レコードがデータベースに反映され、Id も払い出されています。

シグネチャ: .create()

APEX
SOrchestrator orchestrator = SOrchestrator.start()
    .add(/* ... */)
    .add(/* ... */);
orchestrator.create();

振る舞いの要点

  • 内部で トポロジカルソート を実行し、依存される側 (親) から先に insert する
  • .use('alias', 'Id', 'AccountId') のような Id 参照は、親が insert された直後の Id を子のフィールドにコピーしてから子を insert する
  • DML 失敗時は通常の Apex DML 例外がそのまま投げられる
  • 戻り値は void.create().add(...) チェーンの末尾に繋いで変数へ代入することはできない。一旦 SOrchestrator を変数に受けてから orchestrator.create() を呼び、その同じ変数で getByAlias(...) する

.getByAlias(name): 生成後のレコードを取り出す

create() 後、alias で生成済みレコードを取り出せます。SOQL を書かずに「先ほど作った Account」を手元に持ってこられるので、アサーションの記述が短くなります。

シグネチャ: .getByAlias(String aliasName) (戻り値は SObject)

APEX
SOrchestrator orchestrator = SOrchestrator.start()
    .add(
        SBlueprint.of(Account.class)
            .template(Blueprints.accBasic())
            .alias('parentAccount')
    );
orchestrator.create();
 
Account parent = (Account) orchestrator.getByAlias('parentAccount');
Assert.isNotNull(parent.Id);

振る舞いの要点

  • 戻り値は SObject 型なので、利用側で目的の SObject 型にキャストする
  • 存在しない alias を渡すと null が返る (例外ではない)。typo に気付きにくいので、取り出した直後に Assert.isNotNull(...) で守るのが安全
  • .times(n)'{#}' プレースホルダ付きの alias (例: 'con_{#}') で量産したレコードは、'con_1' / 'con_2' / ... のように展開後の alias で個別に取り出せる (詳細は 親子・量産・参照のパターン)

例: 完全なテストワークフロー

startaddcreategetByAlias までを 1 つのテストに通すと、こうなります。

APEX
@isTest
static void testOppCreationWithAccount() {
    SOrchestrator orchestrator = SOrchestrator.start()
        .add(
            SBlueprint.of(Account.class)
                .template(Blueprints.accBasic())
                .alias('parentAccount')
        )
        .add(
            SBlueprint.of(Opportunity.class)
                .set('Name', 'Test Opportunity')
                .set('StageName', 'Prospecting')
                .set('CloseDate', Date.today().addDays(30))
                .use('parentAccount', 'Id', 'AccountId')
                .alias('targetOpp')
        );
    orchestrator.create();
 
    Account parent = (Account) orchestrator.getByAlias('parentAccount');
    Opportunity opp = (Opportunity) orchestrator.getByAlias('targetOpp');
 
    Assert.areEqual(parent.Id, opp.AccountId);
    Assert.areEqual('Test Opportunity', opp.Name);
}

ポイント:

  • テスト本体に SOQL が一行も無い。アサーション対象は getByAlias(...) で直接取り出せている
  • Account を先に .add(...) しているが、仮にここで Opportunity を先に .add(...) しても、SOrchestrator が依存解析で並び替えるので結果は同じ
  • 親 Id は .use('parentAccount', 'Id', 'AccountId') の宣言だけで子に転記される。「親を insert して Id を変数に取り、子の AccountId に代入して...」という手続きは消える

ハマりどころ

循環依存

A.use('B', ...)B.use('A', ...) の両方が成り立つような依存を作ると、トポロジカルソートが解けず Circular or invalid reference detected.create() 時に失敗します。設計を見直して、一方を片方向の参照にするか、親子関係を withChildren に置き換えるかで解消します。

alias の重複

同じ SOrchestrator 内で .alias('foo') が 2 か所にあると Duplicate alias detected で失敗します。.times(n) で量産する場合は 'foo_{#}' のようにプレースホルダ付きで宣言することで、展開後に一意な alias が払い出されます。

存在しない alias の参照

.use('typoAlias', ...) のように、どこにも宣言されていない alias を参照すると、同じく Circular or invalid reference detected 系のエラーで .create() が失敗します。alias の typo は気付きにくいので、「.use の第 1 引数」と「対応する .alias」をペアで見直す習慣をつけると安全です。

自動 alias は取り出し用には使わない

.alias(...) を省略しても blueprint は問題なく登録できますが、内部的に __Account_0_1__ のような 自動 alias が割り振られます。これを後から getByAlias で取り出すのは現実的ではありません。getByAlias で取り出す予定がある blueprint には必ず明示的に .alias(...) を付ける のが原則です。

関連ドキュメント