親子・量産・参照のパターン

Apex Stem ドキュメント
Apex StemApexBlueprintwithChildrentimesRelations
単一レコードの宣言から一歩進めた応用パターン。withChildren のネスト、times + {#} での量産、use の offset 指定、{P0} / {P1} での親祖父母参照、parentIdField による曖昧解消までを実例で解説します。

SBlueprint で単一レコードを宣言する では、1 レコードを単独で宣言する基本 5 メソッドを扱いました。このページでは、そこから一歩進めて 複数レコードを構造的に組み立てる ためのパターンを扱います。

具体的には以下のトピックです:

  • withChildren: 親 blueprint の中に子をネストする
  • times(n) + {#} プレースホルダ: 連番つきの量産
  • use の offset 指定: 量産した親レコードの一部だけを子に紐付ける
  • {P0} / {P1}: ネスト内で「直近の親」「親の親」を参照する
  • parentIdField: 親が複数 lookup を持つときの曖昧解消
  • sharedWith: 手動共有の宣言と、量産への追随

withChildren: 親の下に子をネストする

withChildren(child) は、1 つの blueprint の中に「この blueprint の子レコードたち」を入れ子で書ける API です。コードのインデント階層がそのままデータ階層になるので、親子関係が一目で読み取れます。

1:1 のシンプルな親子

APEX
SBlueprint.of(Account.class)
    .template(Blueprints.accBasic())
    .alias('parentAccount')
    .withChildren(
        SBlueprint.of(Contact.class)
            .set('LastName', 'TestContact')
            .alias('childContact')
    );

ポイント:

  • 子の AccountId への親 Id 転記は 自動.use(...) を書く必要は無い
  • withChildren の引数の中で子の blueprint を インライン定義 することで、「この親の下にぶら下がる子」という構造が視覚化される
  • 子側で .alias(...) を付けておけば、getByAlias('childContact') で取り出せる

1:N の親子 (times との組み合わせ)

withChildren の中で .times(n) を使うと、同じ親に複数の子をぶら下げられます。

APEX
SBlueprint.of(Account.class)
    .alias('parentAccount')
    .template(Blueprints.accBasic())
    .withChildren(
        SBlueprint.of(Contact.class)
            .set('LastName', 'Contact-{#}')
            .alias('con_{#}')
            .times(3)
    );

con_1 / con_2 / con_3 の 3 件の Contact が、すべて同じ Account に紐付いた状態で生成されます。alias と LastName の両方で {#} を使っていることで、後から getByAlias('con_2') で個別に取り出せます。

ネストしてさらに孫まで

withChildrenネスト可能 です。「Account の下に Contact、Contact の下に Case」のような 3 階層も自然に書けます。

APEX
SBlueprint.of(Account.class)
    .alias('acc')
    .template(Blueprints.accBasic())
    .withChildren(
        SBlueprint.of(Contact.class)
            .set('LastName', 'TestContact')
            .alias('con')
            .withChildren(
                SBlueprint.of(Case.class)
                    .set('Subject', 'TestCase')
                    .alias('case')
            )
    );

同じ親の下に複数種類の子を並べる

1 つの親に 異なる SObject 型の子 を並べたい場合は、.withChildren(...) を続けて呼び出します。

APEX
SBlueprint.of(Account.class)
    .template(Blueprints.accBasic())
    .alias('acc')
    .withChildren(
        SBlueprint.of(Contact.class)
            .set('LastName', 'TestContact')
            .alias('childContact')
    )
    .withChildren(
        SBlueprint.of(Opportunity.class)
            .set('Name', 'TestOpportunity')
            .set('StageName', 'Prospecting')
            .set('CloseDate', Date.today().addDays(30))
            .alias('childOpp')
    );

ContactOpportunity が、それぞれ自分の AccountId に親 Id を持って同じ Account の下にぶら下がる形で生成されます。子側で .times(...) も使えるので、「Account の下に Contact 3 件 + Opportunity 2 件」のような不揃いな構造も自然に表現できます。

Multiplication: 上位階層の times が下位に伝播する

withChildren.times(n) を組み合わせるときに重要な仕様があります。上位階層で .times(n) を入れて親を量産すると、その下にぶら下がる子は親ごとに丸ごと再生成される ため、件数は掛け算で増えます。

2 階層: 親 N × 子 M

APEX
SBlueprint.of(Account.class)
    .set('Name', 'Acc-{#}')
    .alias('acc_{#}')
    .times(2)                      // 親 2 件
    .withChildren(
        SBlueprint.of(Contact.class)
            .parentIdField('AccountId')
            .set('LastName', 'Con-{#}')
            .alias('con_{#}')
            .times(2)              // 各親に 2 件ずつ
    );

結果: Account が 2 件、Contact2 × 2 = 4 件 生成されます。

3 階層以上: そのまま指数的に伸びる

階層を増やすと、件数はそのまま掛け算で積み上がっていきます。

APEX
SBlueprint.of(Account.class)
    .times(2)                          // 親 2 件
    .withChildren(
        SBlueprint.of(Contact.class)
            .parentIdField('AccountId')
            .times(2)                  // 子 2 件 / 親 → 合計 4 件
            .withChildren(
                SBlueprint.of(Case.class)
                    .parentIdField('ContactId')
                    .times(2)          // 孫 2 件 / 子 → 合計 8 件
            )
    );

総レコード件数: Account 2 件 + Contact 4 件 + Case 2 × 2 × 2 = 8 件。「階層の .times(...) 値の積」が末端のレコード件数になる、と覚えておけば見通しが立てやすくなります。

階層が深くなると レコード数が指数的に増える ことに注意してください。5 階層で各 .times(3) を重ねると 3⁵ = 243 件のレコードが生成され、ガバナ制限の DML 行数 (10000) を圧迫します。「ちょっと多めに」のつもりが膨大な件数に化けやすいので、各階層の .times(...) は意識的に絞り込むのが安全です。

times + {#}: 連番つきの量産

.times(n) は同じ blueprint を n 件量産します。「3 件の Contact」「10 件の Account」のようなケースで、for ループを書かずに 1 行で済ませられます。

{#} プレースホルダは .set(...) の値や .alias(...) の引数に埋め込んで使います。times(n) で展開されるとき、{#}1, 2, 3, ... と置き換わります。

APEX
SBlueprint.of(Contact.class)
    .set('LastName', 'Contact-{#}')
    .alias('con_{#}')
    .times(3);

生成されるのは:

aliasLastName
con_1Contact-1
con_2Contact-2
con_3Contact-3

アルファベット連番: {A} / {a}

{#} の数値連番に加えて、アルファベット連番 用の {A} / {a} プレースホルダもサポートされています。大文字版が {A}'A' / 'B' / 'C' ...、小文字版が {a}'a' / 'b' / 'c' ... に展開されます。

APEX
SBlueprint.of(Account.class)
    .set('Name', 'Acc-{A}')
    .alias('acc_{a}')
    .times(3);

生成されるのは:

aliasName
acc_aAcc-A
acc_bAcc-B
acc_cAcc-C

「人間が読んで違いを判別したい」ようなテストデータ (テストレポートに出るラベル等) で、数字より文字記号の方が読みやすい場合に使い分けます。

startAt / interval: 連番の起点と刻みを変える

.set(...) / .alias(...) には、{#}起点刻み を指定する追加引数があります。

APEX
SBlueprint.of(Account.class)
    .set('Name', 'Acc-{#}', 10, 2)   // 10, 12, 14
    .alias('acc_{#}', 10, 2)         // acc_10, acc_12, acc_14
    .times(3);

「N 件目から始まる連番が欲しい」「偶数番号だけ作りたい」のような細かい要件にも対応できます。

use の offset 指定: 一部の親だけを子に紐付ける

.times(n)量産した親レコード の中から、一部だけを子に紐付けたいケースがあります。例えば「10 件の Account のうち、後半 5 件 (6 〜 10) だけに Contact を紐付ける」のような要件です。

use には 4-5 引数のオーバーロードがあり、参照する alias の 開始番号刻み を指定できます。

シグネチャ: .use(String alias, String fromField, String toField, Integer startAt [, Integer interval])

APEX
SOrchestrator.start()
    .add(
        SBlueprint.of(Account.class)
            .alias('acc_{#}')
            .template(Blueprints.accBasic())
            .times(10)                              // acc_1 〜 acc_10
    )
    .add(
        SBlueprint.of(Contact.class)
            .set('LastName', 'Con-{#}')
            .alias('con_{#}', 6)                    // con_6 〜 con_10
            .use('acc_{#}', 'Id', 'AccountId', 6)   // acc_6 〜 acc_10 を参照
            .times(5)
    );

生成される子は con_6con_10 の 5 件で、それぞれ acc_6acc_10 を親に持つ形になります。「全件揃って同じ親を持つ」形ではなく、「不揃いな対応」を表現するのにこのオーバーロードが効きます。

{P0} / {P1} / ...: 上位階層の値を参照する

なぜこのプレースホルダが必要か

各階層で .times(...) を使ったネストでは、末端のレコードから見た「自分の真の親」は 生成のたびに変わるインスタンス になります。たとえば:

  • 親 (Account) .times(2)
  • 子 (Contact) .times(2)
  • 孫 (Case) .times(2)

このとき孫は計 8 件、子は 4 件 (各親に 2 件ずつ)、親は 2 件です。各孫から見た「自分の真の親 (Contact)」は 4 件の Contact の中の特定の 1 件です。

ところが Contact 側で .alias('child_{#}') のように単純な {#} プレースホルダで alias を付けると、「Acme-1 の下の child_1」と「Acme-2 の下の child_1」が 同じ alias を二重宣言したことになり、alias 重複エラーで実行が止まります

抜け道はあります。.alias('{P0}_child_{#}') のように 親の alias を埋め込んだ複合 alias を作れば、__Account_0_1___child_1 / __Account_0_1___child_2 / __Account_0_2___child_1 / __Account_0_2___child_2 のような 4 通りの一意な alias が払い出され、孫から .use('__Account_0_1___child_1', ...) のように指せます。

ただ、この方針には欠点があります:

  • 孫を .use(...) する側で 「自分の真の親の alias 文字列」を頭の中で組み立てる 必要が出る
  • 階層構造を後で変更すると、alias 文字列の組み立てロジックも全部書き直し
  • 結果として、テストコードが「データ構造」ではなく「alias 文字列パズル」のように読めてしまう

これを構造的に避けるのが {P0} / {P1} / {P2} ... プレースホルダです。「上から N 番目 (0 始まり) の階層にある、自分にとっての真の親」を SOrchestrator が内部で階層的に自動解決してくれるので、alias を打つ必要も、alias 文字列を頭で組み立てる必要もありません。

数え方: ルートからの絶対深度

数え方は 「自分から上方向への距離」ではなく、ルートから数えた絶対深度 です:

  • {P0}: ルート (一番外側の親、階層 0)
  • {P1}: ルートの 1 つ下 (階層 1)
  • {P2}: さらにその下 (階層 2)
  • 以下、階層が深くなるごとに番号が増える

つまり「4 階層目から階層 1 の値を参照したい」ときは {P1} を指定します (「現在地から上に 3 つ」ではないので注意)。

最小例: 2 階層で {P0} を使う

まずは最もシンプルな 2 階層構造で、{P0} (= ルート) を参照するパターンから見ます。子 Contact の Description に親 Account の Name を流し込むだけのケースです。

APEX
SBlueprint.of(Account.class)             // P0 (ルート)
    .set('Name', 'Acme')
    .withChildren(
        SBlueprint.of(Contact.class)
            .set('LastName', 'TestContact')
            .use('{P0}', 'Name', 'Description')  // ルート Account の Name を Description に
    );

Contact の Description には 'Acme' が入ります。{P0} が「ルート Account」を指していて、親側で .alias(...) を打つ必要がありません。

「親を 1 件作って、その値を子に引き写すだけ」という用途であれば、ここまでの理解で十分です。階層が深くなって .times(...) で量産が絡んだ、より高度な使い方は次の Example で見ます。

例: 孫が「自分の真の親」の値を引き写す

3 階層 (親 / 子 / 孫) で、孫 Case の Subject自分の真の親 Contact の LastName を入れます。

APEX
SBlueprint.of(Account.class)                            // P0 (階層 0 / ルート)
    .set('Name', 'Acme-{#}')
    .times(2)
    .withChildren(
        SBlueprint.of(Contact.class)                    // P1 (階層 1)
            .parentIdField('AccountId')
            .set('LastName', 'Contact-{#}')
            .times(2)
            .withChildren(
                SBlueprint.of(Case.class)               // P2 (階層 2 = 孫 Case)
                    .parentIdField('ContactId')
                    .use('{P1}', 'LastName', 'Subject') // ← 自分の真の親 Contact の LastName
                    .times(2)
            )
    );

生成されるレコードと、孫 Case の Subject (= 自分の真の親 Contact の LastName) は次のようになります。

AccountContact (真の親)Case (孫) Subject
Acme-1Contact-1 (Acme-1 の下)Contact-1
Acme-1Contact-2 (Acme-1 の下)Contact-2
Acme-2Contact-1 (Acme-2 の下)Contact-1
Acme-2Contact-2 (Acme-2 の下)Contact-2

それぞれの Contact の下に孫 2 件ずつあるので、孫 Case は計 8 件、上記の組 4 通り × 各 2 件ずつ、という分布になります。同じ Contact-1 という LastName を持つ Contact が 2 件 (Acme-1 の下 / Acme-2 の下) に存在しますが、各孫は 自分にぶら下がっている本物の親 から値を受け取ります。

ポイント:

  • alias を打たなくても、「自分にとっての真の親」が階層的に自動解決される
  • 数え方はルートからの絶対深度 (現在地から上方向への距離ではない)
  • 親 Id の転記は withChildren の自動処理に任せ、任意のフィールド値を引き写したいときだけ {Pn} を使う

parentIdField: 親が複数 lookup を持つときに明示する

子オブジェクトが 複数の lookup 候補 を持っている場合 (例: ContactAccountIdCustomAccount__c の両方を持つ)、withChildren だけでは「どちらの lookup に親 Id を入れるか」を SOrchestrator が判断できません。このとき .parentIdField(...) で明示します。

シグネチャ: .parentIdField(String fieldName)

APEX
SBlueprint.of(Account.class)
    .alias('acc')
    .withChildren(
        SBlueprint.of(Contact.class)
            .parentIdField('AccountId')    // どの lookup に親 Id を入れるかを明示
            .template(Blueprints.conBasic())
    );

複数 lookup が無いオブジェクトでは .parentIdField(...) を書く必要はありません。「曖昧でエラーになったら付ける」という後付けの保険として覚えておけば十分です。

sharedWith: 共有も量産に追随する (v2.0.0+)

.sharedWith(user, accessLevel) は手動共有を最終状態として宣言します。Foo__Share / AccountShare のレコードを自分で組み立てる必要も、親より後に insert する順序を気にする必要もありません。

シグネチャ: .sharedWith(User user, String accessLevel) (accessLevel'Read' または 'Edit')

このページの文脈で重要なのは、共有宣言がここまで説明してきた量産の仕組みにそのまま乗ることです。times で増えた親には、共有も同じ数だけ増えます。

APEX
SBlueprint.of(Invoice__c.class)
    .set('Name', 'Invoice-{#}')
    .alias('inv_{#}')
    .owner(admin)
    .sharedWith(rep, 'Read')
    .times(5);
// → Invoice__c 5 件と、それぞれに対応する Invoice__Share 5 件

ネストした子に付けた共有も同じで、親の times に掛け算されて増えます

APEX
SBlueprint.of(Account.class)
    .set('Name', 'Acme-{#}')
    .times(2)
    .withChildren(
        SBlueprint.of(Invoice__c.class)
            .owner(admin)
            .sharedWith(rep, 'Read')
            .times(3)          // 請求書 6 件 → 共有も 6 件
    );

これは共有だけの特別扱いではありません。sharedWith は内部で use() で親に繋がれた兄弟 blueprint を組み立てているだけなので、times の伝播も {Pn} の解決も、通常の子と完全に同じ経路をたどります (詳細は 依存解決の仕組み)。

宣言が矛盾していれば DML の前に落ちる

共有は「書けば必ず通る」ものではないので、宣言の時点で成立しないケースは create() の解析段階で ApexBlueprintException になります。

状況理由
組織の共有設定 (OWD) が Public のオブジェクト手動共有レコード自体が存在しない (Foo__Share が見つからない)
owner() で指定した本人への共有オーナーは既に全権を持つため、Salesforce が手動共有を拒否する
accessLevel'Read' / 'Edit' 以外指定ミス

いずれも DML には到達しません。「insert してみたら共有が入っていなかった」ではなく、宣言した時点で理由つきで止まります。

📌 共有をテストする相手側のユーザー (ペルソナ) の作り方は SPersona の API リファレンス を参照してください。

例: 親 1 件 + 子 3 件 + 孫 1 件

これまでのパターンを 1 つのテストに統合した例です。

APEX
@isTest
static void testAccountWithContactsAndCase() {
    SOrchestrator orchestrator = SOrchestrator.start()
        .add(
            SBlueprint.of(Account.class)
                .template(Blueprints.accBasic())
                .set('Name', 'ParentAccount')
                .alias('acc')
                .withChildren(
                    SBlueprint.of(Contact.class)
                        .set('LastName', 'Contact-{#}')
                        .use('{P0}', 'Name', 'Description')  // ルート Account (P0) の Name を Description に
                        .alias('con_{#}')
                        .times(3)
                )
        )
        .add(
            SBlueprint.of(Case.class)
                .set('Subject', 'TestCase')
                .use('con_2', 'Id', 'ContactId')             // 2 番目の Contact に紐付け
                .alias('targetCase')
        );
    orchestrator.create();
 
    Account parent = (Account) orchestrator.getByAlias('acc');
    Contact con2 = (Contact) orchestrator.getByAlias('con_2');
    Case targetCase = (Case) orchestrator.getByAlias('targetCase');
 
    Assert.areEqual(3, [SELECT COUNT() FROM Contact WHERE AccountId = :parent.Id]);
    Assert.areEqual('ParentAccount', con2.Description);
    Assert.areEqual(con2.Id, targetCase.ContactId);
}

ポイント:

  • withChildren のネストで「Account に Contact が 3 件ぶら下がる」構造をそのまま視覚化
  • use('{P0}', 'Name', 'Description') で親 Account の Name を子 Contact の Description に流し込み
  • {#} 付きの alias (con_{#}) で、後から 'con_2' という形で 2 番目の Contact を取り出して Case に紐付け
  • テスト本体には複雑な手続きが無く、「最終的に作りたいデータ構造」がそのまま読める

例: 木をまたぐ共有参照 (ダイヤ型の依存)

withChildren は「木」を表現しますが、実際の結合テストでは 木の外にある共有レコードを、深くネストした子から参照したい ことがあります。たとえば Account → Opportunity → Quote → QuoteLineItem の 4 階層で、最深部の QuoteLineItem が共有の Product2 を参照するようなケースで、これは木ではなく「ダイヤ型」(DAG) の依存になります。

親子の鎖は withChildren で、木をまたぐ参照は use で宣言し、共有レコードは別の add で登録して alias で参照するだけです。add の順序は自由で、SOrchestrator が「Product2AccountQuoteLineItem より先に」とトポロジカルソートで並べてくれます。

APEX
@isTest
static void testQuoteLineItemReferencesSharedProduct() {
    SOrchestrator orchestrator = SOrchestrator.start()
        .add(
            SBlueprint.of(Product2.class)
                .set('Name', 'Widget')
                .alias('product')                        // 木の外に共有レコード
        )
        .add(
            SBlueprint.of(Account.class)
                .template(Blueprints.accBasic())
                .withChildren(
                    SBlueprint.of(Opportunity.class)
                        .template(Blueprints.oppBasic())
                        .withChildren(
                            SBlueprint.of(Quote.class)
                                .set('Name', 'Q-2026')
                                .withChildren(
                                    SBlueprint.of(QuoteLineItem.class)
                                        .template(Blueprints.qliBasic())
                                        .set('Quantity', 3)
                                        .use('product', 'Id', 'Product2Id')  // 木をまたぐ参照
                                        .alias('qli')
                                )
                        )
                )
        );
    orchestrator.create();
 
    Product2 product = (Product2) orchestrator.getByAlias('product');
    QuoteLineItem qli = (QuoteLineItem) orchestrator.getByAlias('qli');
    Assert.areEqual(product.Id, qli.Product2Id);
}

ポイント:

  • 共有レコード (Product2) は木の枝ではないので、withChildren ではなく 別の add + alias で登録する
  • 木をまたぐ参照は use('product', 'Id', 'Product2Id') の 1 行で宣言。親子の鎖 (withChildren) と組み合わせると、依存グラフは木ではなく「ダイヤ型」になる
  • add の順序は読みやすい順でよい。insert 順序は SOrchestrator がトポロジカルソートで解決するので、利用者は一切気にしない
  • QuoteLineItem の必須項目 (PricebookEntryId / UnitPrice など、実際には PricebookEntry を別途 add で用意する必要がある) は Blueprints.qliBasic()template に寄せ、テスト本体には検証したい差分 (Quantity) と参照 (use) だけを残す

関連ドキュメント