cd ~/blog/salesforce-governor-limits

2026-06-25

外部オブジェクトを参照する Apex 一括登録 — Queueable とガバナ制限対策

SalesforceApexガバナ制限QueueableSOQL

はじめに

Salesforce のカスタムオブジェクトに複数件のデータを一括登録する機能を実装・運用していたところ、「30件以上を登録しようとするとエラーが発生する」 という報告が来ました。

エラーの内容は Apex の System.LimitException、いわゆるガバナ制限です。30件という妙に中途半端な閾値が気になりつつも、最初は単純な実装ミスだと思っていました。原因を特定して修正すると、今度は別の LimitException が発生。またそれを修正すると、さらに別の制限に引っかかる。最終的に、3種類のガバナ制限が連鎖していることが判明しました。

この記事では、その原因の調査・修正の過程と、同様の実装をする際に参考になる設計パターンをまとめます。


背景

「30件を超えるとエラーになる」という報告を受けたとき、最初は単純な実装ミスだと思っていました。ところが修正するたびに別のガバナ制限が顔を出し、結局3種類の制限が連鎖していたことが判明しました。

実装していたのは Salesforce のカスタムオブジェクトへのデータ一括登録機能です(最大 200 件)。登録対象のレコードを確定するには外部オブジェクトを複数参照する必要があります。外部オブジェクトへのアクセスはネットワーク越しの問い合わせになるため、件数が増えるほど応答に時間がかかります。これを同期処理にすると、その待機時間がそのまま画面の応答待ちに直結し、UX が著しく低下します。そこで 外部オブジェクトの参照処理は Queueable で非同期化する という設計を採りました。

この構成が、3つのガバナ制限を次々と引き起こす温床になっていました。

オブジェクト A は不要ではないかと思われるかもしれませんが、こちらは入力された複数件のデータに対して登録前に重複チェックを行う要件があったためです。オブジェクト A に一括保存することで既存レコードとの照合が可能になり、その ID を元に Queueable でオブジェクト B を生成しています。


Act 1: 第一の壁 — 同期SOQLが多すぎる

調査

エラーログを見ると Apex の System.LimitException: Too many SOQL queries: 101 でした。同期 Apex で発行できる SOQL の上限は 100 件。それを超えていました。

登録処理の実装を読むと、for 文の中に SOQL が 3 つ埋まっていました。

APEX
for (InputRecord input : inputs) {
    // ① 入力コードの存在確認
    List<ObjectA__c> records = [SELECT Id FROM ObjectA__c WHERE Code__c = :input.code];
    // ② 関連するキューオブジェクトの取得
    List<QueueSobject> queues = [SELECT QueueId FROM QueueSobject WHERE ...];
    // ③ 複数条件による重複チェック
    List<ObjectA__c> duplicates = [SELECT Id FROM ObjectA__c WHERE ...];

    // ... バリデーションと登録処理
}

これでは件数に比例して SOQL が増えます。33件までは制限内に収まりますが、34件以上になった瞬間に超えます。

3 × 33 = 99 SOQL → 制限 100 以内 ✅

3 × 34 = 102 SOQL → 制限 100 超 ❌

修正

SOQL を for 文の外に出し、事前に必要なデータを一括取得してから Map に格納する方法に変えました。

APEX
// 事前に一括取得
Map<String, ObjectA__c> recordMap = new Map<String, ObjectA__c>();
for (ObjectA__c r : [SELECT Id, Code__c FROM ObjectA__c WHERE Code__c IN :codes]) {
    recordMap.put(r.Code__c, r);
}
// キュー・重複チェックも同様に事前取得 ...

for (InputRecord input : inputs) {
    ObjectA__c record = recordMap.get(input.code); // Map 参照は SOQL にカウントされない
    // ...
}

件数に関わらず SOQL は 3 回で済みます:

これで解決したと思っていました。

INSERT などの DML 文は SOQL クエリとは別のガバナ制限(非同期: DML 150 件)でカウントされます。SOQL クエリ数の制限には影響しないため、今回の計算式には含めていません。


Act 2: 第二の壁 — enqueueJob が多すぎる

調査

ところが、テストするとまた別のエラーが出ましたSystem.LimitException: Too many queueable jobs added to the queue: 51System.enqueueJob で積み上げられる非同期ジョブの上限は 50 件です。

問題はオブジェクト A が登録された後に起動するフローの中の Apex でした。

APEX
@InvocableMethod(...)
public static void createRelatedRecords(List<Id> recordIds) {
    for (Id recordId : recordIds) {
        System.enqueueJob(new ObjectBCreationJob(recordId)); // 1件ずつキュー追加
    }
}

レコードトリガーフローは、1 トランザクションで複数レコードが登録された場合でも 1 回のフロー実行にまとめられます。つまり 200 件登録すると、この for 文が 200 回実行され、200 個の enqueueJob が呼ばれます。

51件を超えた時点で制限に引っかかります。

200 enqueueJob → 制限 50 を大幅に超える ❌

修正

1 レコードずつキューに積むのをやめ、複数レコードをまとめて処理する チャンク方式に変えました。

APEX
@InvocableMethod(...)
public static void createRelatedRecords(List<Id> recordIds) {
    Integer chunkSize = 50;
    for (Integer i = 0; i < recordIds.size(); i += chunkSize) {
        Integer end = Math.min(i + chunkSize, recordIds.size());
        List<Id> chunk = new List<Id>(recordIds.subList(i, end));
        System.enqueueJob(new ObjectBCreationJob(chunk));
    }
}

200 件をチャンクサイズ 50 で処理すると:

200 / 50 = 4 enqueueJob → 制限 50 以内 ✅

今度こそ解決したと思っていました。


Act 3: 第三の壁 — 非同期 SOQL も多すぎる

調査

またエラーが出ました。System.LimitException: Too many SOQL queries: 201。今度は非同期 Apex(Queueable)内の SOQL 制限です。同期の倍にあたる 200 件が上限ですが、それでも超えていました。

ObjectBCreationJob の中を見ると、オブジェクト B を生成するための事前調査として 1 レコードあたり 12 個の SOQL を発行していました。チャンクサイズ 50 の場合、1 回の Queueable 実行で 12 × 50 = 600 件の SOQL が発行されており、制限の 200 を大幅に超えていました。

カテゴリSELECT 数
登録対象レコードの取得1
関連オブジェクトの参照4
各種マスタデータの参照5
設定・制御情報の参照2
合計12

1 Queueable で発行される SOQL の合計は 12 × チャンクサイズ です。これが非同期 SOQL の制限 200 以内に収まる条件を整理すると:

12 × チャンクサイズ ≦ 200 ⟹ チャンクサイズ ≦ 16.666...

つまりチャンクサイズは 16 以下 でなければなりません。一方、Act 2 の enqueueJob 制限からは:

200 / チャンクサイズ ≦ 50 ⟹ チャンクサイズ ≧ 4

両方を同時に満たす必要があります。

4 ≦ チャンクサイズ ≦ 16

修正

チャンクサイズを 10 に設定することで、両方のガバナ制限をクリアできます。

200 / 10 = 20 enqueueJob → 制限 50 以内 ✅

12 × 10 = 120 SOQL → 制限 200 以内 ✅

チャンクサイズを 10 に変更してデプロイしました。ようやく、エラーが出なくなりました。


まとめ

今回対処したガバナ制限を整理すると次の通りです。

制限上限対策
同期 SOQL100for 文の外で事前一括取得
enqueueJob50チャンク単位でジョブを起動
非同期 SOQL200チャンクサイズを逆算して設定

ただし、今回の実装が正しい設計だとは言い切れません。

今回はたまたま最大件数が 200 件という要件だったため、チャンクサイズを 4〜16 の範囲に収めることができました。しかし最大件数がさらに多かったり、1 レコードあたりの SOQL 数が増えたりすれば、チャンク処理だけでは対応できなくなります。外部オブジェクトをビューにまとめて発行回数を減らす方法や、別の非同期処理パターンを選ぶべきケースもあるかもしれません。Salesforce においてこのような要件に対する最適な設計が何なのかは、正直なところまだよく分かっていません。

この記事で伝えたいのは、具体的な設計パターンよりもガバナ制限を設計段階から意識するという教訓です。外部オブジェクトを参照する処理を組み込む場合、1 トランザクションの SOQL 数キューに積むジョブ数1 ジョブあたりの SOQL 数は事前に見積もっておかないと、本番に近い件数でしか再現しないエラーとして後から顕在化します。

もし「こういう設計のほうが筋がいい」「Salesforce ではこのパターンが定石だ」という知見をお持ちの方がいれば、ぜひ教えていただけると嬉しいです。


参照