Salesforce Queueable Apex 徹底解説:非同期処理のベストプラクティス

背景と適用シナリオ

Salesforce プラットフォーム上で複雑なビジネスロジックを開発する際、我々は常にガバナ制限 (Governor Limits) という壁に直面します。これは、マルチテナント環境である Salesforce が、すべてのユーザーに安定したパフォーマンスを提供するための重要な仕組みです。1つのトランザクション内で実行できる SOQL クエリの数、DML ステートメントの数、そして CPU 処理時間などには厳しい上限が設けられています。

トリガーや Visualforce コントローラー内で重量な処理を実行しようとすると、これらの制限に抵触し、エラーが発生する可能性があります。このような課題を解決するために Salesforce が提供するのが、非同期 Apex (Asynchronous Apex) です。非同期 Apex は、処理をバックグラウンドで実行させることで、現在のトランザクションをガバナ制限から解放します。

Salesforce にはいくつかの非同期処理の選択肢があります:

  • Future メソッド (@future): 最もシンプルで古くからある非同期処理です。メソッドに @future アノテーションを付与するだけで利用できますが、パラメータとして渡せるのはプリミティブ型(String, Integer など)やそのコレクションのみで、sObject を直接渡すことはできません。また、ジョブの連鎖(ある非同期ジョブが完了したら次のジョブを開始する)もできず、実行状況を監視するための Job ID も取得できません。
  • Batch Apex: 数万から数百万件といった大量のレコードを処理するために設計されています。データを小さな「バッチ」に分割して処理するため、巨大なデータセットに対しても安定して動作します。しかし、設定が比較的複雑であり、単純な非同期処理には大げさな場合があります。
  • Scheduled Apex: 特定の時刻や間隔で定期的にジョブを実行したい場合に使用します。

そして、本稿の主役である Queueable Apex は、Future メソッドのシンプルさと Batch Apex の持つパワーの間に位置する、非常に強力で柔軟な非同期処理の仕組みです。Queueable Apex は以下のようなシナリオで特に輝きを放ちます。

  • 複雑な sObject の受け渡し: Future メソッドとは異なり、Queueable Apex のクラスインスタンスにはメンバー変数として sObject を保持できるため、複雑なデータ構造をそのまま非同期処理に渡すことができます。
  • ジョブの連鎖 (Chaining): ある Queueable ジョブの処理が完了した時点で、次の Queueable ジョブをキューに追加することができます。これにより、依存関係のある一連の非同期処理を順番に実行することが可能になります。
  • ジョブの監視: System.enqueueJob() メソッドは、キューに追加されたジョブの ID (Job ID) を返します。この ID を使用して、AsyncApexJob オブジェクトを照会し、ジョブの実行状況(待機中、処理中、完了、失敗など)を追跡できます。
  • 外部システムへのコールアウト: Database.AllowsCallouts インターフェースを実装することで、Queueable ジョブから外部システムへの HTTP コールアウトを実行できます。これにより、トリガーのコンテキストから直接コールアウトを行うといった、通常は許可されない操作を安全に実現できます。

原理説明

Queueable Apex の実装は非常に直感的です。その中心となるのは Queueable という名前のインターフェースです。

このインターフェースを Apex クラスに実装 (implement) すると、そのクラスは非同期ジョブとしてキューに追加できるようになります。Queueable インターフェースには、実装が必須なメソッドが一つだけ定義されています。

execute(QueueableContext context)

この execute メソッドが、非同期処理の本体です。Salesforce プラットフォームがジョブをキューから取り出して実行する際に、このメソッドが呼び出されます。引数として渡される QueueableContext オブジェクトは、現在のジョブの ID を取得するための getJobId() メソッドを提供しますが、現時点ではそれ以外の機能はありません。

Queueable クラスのインスタンスを作成し、それを非同期実行のキューに追加するには、System.enqueueJob() メソッドを使用します。

MyQueueableClass myJob = new MyQueueableClass();
ID jobId = System.enqueueJob(myJob);

このメソッド呼び出しにより、myJob インスタンスはシリアライズされ、プラットフォームのキューに登録されます。Salesforce はリソースが利用可能になった時点でこのジョブをデキュー(キューから取り出し)し、デシリアライズして execute メソッドを実行します。重要なのは、execute メソッドが実行されるとき、それは完全に新しいトランザクションとして扱われ、独自のガバナ制限が適用されるという点です。

Queueable Apex の最も強力な機能の一つであるジョブの連鎖 (Job Chaining) は、execute メソッド内で再び System.enqueueJob() を呼び出すことで実現されます。例えば、Job A の処理が完了した後に Job B を実行したい場合、Job A の execute メソッドの最後に Job B をエンキューします。

// Job A の execute メソッド内
public void execute(QueueableContext context) {
    // Job A の処理ロジック...

    // 処理完了後、次のジョブ (Job B) をキューに追加
    System.enqueueJob(new JobB());
}

このシンプルな仕組みにより、複雑なワークフローを非同期で構築することが可能になります。


示例代码

ここでは、Salesforce の公式ドキュメントに記載されている、ジョブの連鎖を示す典型的なサンプルコードを紹介します。この例では、取引先 (Account) に主担当者 (Contact) を設定し、その後、後続のジョブを呼び出します。

第一のジョブ: AddPrimaryContact

このクラスは、特定の取引先責任者を取引先の主担当者として更新するジョブです。処理完了後、SecondJob という名前の別の Queueable ジョブをキューに追加します。

// 公式ドキュメントに基づくサンプルコード
// 参照: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_queueing_jobs.htm
public class AddPrimaryContact implements Queueable {
    private Contact contact;
    private String state;

    // コンストラクタ: 非同期処理に必要なデータを受け取る
    public AddPrimaryContact(Contact contact, String state) {
        this.contact = contact;
        this.state = state;
    }

    // execute メソッド: 非同期処理の本体
    public void execute(QueueableContext context) {
        // 指定された州(state)に一致する取引先を検索
        // この例では簡略化のため、最初の1件のみを対象とする
        List<Account> accounts = [SELECT Id, Name FROM Account WHERE BillingState = :state LIMIT 1];

        if (!accounts.isEmpty()) {
            Account account = accounts[0];
            
            // 渡された取引先責任者を、見つかった取引先の主担当者として設定する
            // 実際には、より複雑なロジックがここに入る
            Contact primaryContact = new Contact(
                Id = this.contact.Id,
                AccountId = account.Id
                // ... 他のフィールド更新
            );
            update primaryContact;
        }

        // --- ジョブの連鎖 ---
        // このジョブの処理が完了したら、次のジョブ (SecondJob) をキューに追加する
        System.enqueueJob(new SecondJob());
    }
}

第二のジョブ: SecondJob

これは、最初のジョブから連鎖的に呼び出される単純なジョブです。

// 公式ドキュメントに基づくサンプルコード
public class SecondJob implements Queueable {
    public void execute(QueueableContext context) {
        // 後続の処理を実行
        // 例: ログの記録、別のオブジェクトの更新、外部システムへの通知など
        System.debug('Second job executed.');
    }
}

ジョブの起動

これらのジョブを起動するには、以下のような Apex コード(例えば、匿名実行ウィンドウ)を実行します。

// 匿名実行などから最初のジョブを起動する
// 1. テスト用の取引先を作成
Account a = new Account(Name='Acme', BillingState='CA');
insert a;

// 2. テスト用の取引先責任者を作成
// この時点では取引先とは紐付けない
Contact c = new Contact(FirstName='John', LastName='Doe');
insert c;

// 3. Queueable クラスのインスタンスを作成し、データを渡す
AddPrimaryContact job = new AddPrimaryContact(c, 'CA');

// 4. System.enqueueJob でジョブをキューに追加し、Job ID を取得
ID jobId = System.enqueueJob(job);

// 5. Job ID を使用してジョブのステータスを確認できる
System.debug('Queueable job ID is: ' + jobId);

このコードを実行すると、まず AddPrimaryContact ジョブがキューに追加されます。Salesforce がこのジョブを実行すると、カリフォルニア州 (CA) の取引先が検索され、John Doe という取引先責任者がその取引先に紐付けられます。そして、execute メソッドの最後で SecondJob がキューに追加され、後続の処理が実行されます。


注意事項

Queueable Apex を使用する際には、いくつかの重要な制約と考慮事項があります。

ガバナ制限とキューの深さ

  • キュー追加制限: 1回のトランザクション内で System.enqueueJob() を呼び出せるのは、最大50回までです。
  • 連鎖の制限: 最も重要な制限の一つです。非同期処理のコンテキスト(Queueable, Batch, Future の execute メソッド内)からは、System.enqueueJob()1回しか呼び出すことができません。これは、無限ループや指数関数的なジョブの増殖を防ぐための安全装置です。したがって、1つのジョブから複数のジョブを並列で起動するような設計はできません。
  • スタックの深さ: 同期処理から非同期処理を呼び出す際にも、ジョブの連鎖には深さ制限があります。本番環境と Sandbox 環境で挙動が異なる場合があるため、無限に連鎖するような設計は避けるべきです。

テスト

Queueable ジョブのテストは、Test.startTest()Test.stopTest() を使用して行います。Test.startTest()Test.stopTest() のブロック内に System.enqueueJob() を記述すると、Test.stopTest() が実行された時点で、キューに追加されたジョブが同期的に実行されます。これにより、非同期処理の結果をテストクラス内で検証することが可能になります。

@isTest
private class AddPrimaryContactTest {
    @isTest
    static void testQueueable() {
        // 1. テストデータの設定
        Account a = new Account(Name='Test Account', BillingState='NY');
        insert a;
        Contact c = new Contact(FirstName='Test', LastName='Contact');
        insert c;

        Test.startTest();
        // 2. テスト対象のジョブをキューに追加
        System.enqueueJob(new AddPrimaryContact(c, 'NY'));
        Test.stopTest(); // ここでキュー内のジョブが同期的に実行される

        // 3. 結果の検証
        Contact updatedContact = [SELECT AccountId FROM Contact WHERE Id = :c.Id];
        System.assertEquals(a.Id, updatedContact.AccountId, 'Contact should be assigned to the account.');
    }
}

エラーハンドリング

execute メソッド内で捕捉されない例外が発生した場合、ジョブは失敗し、そのトランザクションで行われたすべての DML 操作はロールバックされます。本番環境で安定した運用を行うためには、execute メソッド全体を try-catch ブロックで囲み、エラーを適切にログに記録したり、管理者に通知したりする仕組みを実装することが不可欠です。

外部コールアウト

Queueable ジョブから外部システムへ HTTP コールアウトを行うには、クラス定義で Database.AllowsCallouts インターフェースを実装する必要があります。これを忘れると、コールアウト実行時にエラーが発生します。

public class MyCalloutQueueable implements Queueable, Database.AllowsCallouts {
    public void execute(QueueableContext context) {
        // ここで HTTP コールアウトを実行できる
        HttpRequest req = new HttpRequest();
        // ...
    }
}

まとめとベストプラクティス

Queueable Apex は、Salesforce における非同期処理の強力な選択肢であり、Future メソッドの制約を克服し、Batch Apex ほどの複雑さを必要としない多くのシナリオで理想的なソリューションです。

非同期処理の使い分け:

  • Future メソッド (@future): 最も手軽な非同期処理。ジョブの監視や連鎖が不要で、単純な「撃ちっぱなし」の処理(特にトリガーからのコールアウトなど)に適しています。
  • Queueable Apex: 現代の非同期処理の第一選択肢。sObject を渡したい、ジョブを連鎖させたい、Job ID で進捗を監視したい場合に最適です。
  • Batch Apex: 数万件以上の大量のレコードを分割して処理する必要がある場合にのみ使用します。
  • Scheduled Apex: 毎日深夜や毎週月曜の朝など、決まったスケジュールで処理を実行したい場合に使用します。

ベストプラクティス:

  1. ロジックの分離: Queueable クラスの責務は、非同期処理の枠組みを提供することに留めます。実際のビジネスロジックは、別のヘルパークラスに実装し、execute メソッドから呼び出すように設計することで、コードの再利用性とテストの容易性が向上します。
  2. 堅牢なエラーハンドリング: 必ず try-catch を実装し、例外発生時には詳細なエラーログ(カスタムオブジェクトやプラットフォームイベントなど)を残し、必要に応じて管理者に通知する仕組みを構築してください。
  3. 冪等性 (Idempotency) の担保: ジョブが何らかの理由で再試行される可能性も考慮し、処理が複数回実行されても問題が発生しないように(冪等性を保つように)設計することが望ましいです。
  4. ガバナ制限の意識: execute メソッド内もガバナ制限の対象です。単一のジョブ内で大量のレコードを処理しようとせず、必要であればジョブをさらに連鎖させて処理を分割するなどの工夫が必要です。
  5. 網羅的なテスト: 正常系だけでなく、データが見つからない場合や予期せぬエラーが発生した場合など、異常系のシナリオも含めたテストクラスを作成し、コードカバレッジだけでなく、ビジネスロジックの堅牢性を担保してください。

Queueable Apex を適切に理解し、活用することで、Salesforce プラットフォームのガバナ制限を乗り越え、よりスケーラブルで堅牢なアプリケーションを構築することが可能になります。

コメント