Salesforce Queueable Apexをマスターする:非同期処理の徹底解説

背景と応用シナリオ

Salesforceプラットフォーム上でアプリケーションを開発する際、我々開発者が常に直面する課題の一つが Governor Limits (ガバナ制限) です。これは、プラットフォームのリソースをすべてのユーザーで公平に共有するための、トランザクションあたりのリソース消費量(SOQLクエリ数、DML操作数、CPU時間など)に対する厳格な制限です。ユーザーがボタンをクリックしたり、レコードを保存したりする際の同期的な処理では、これらの制限に容易に抵触してしまう可能性があります。

特に、大量のレコードを処理したり、外部システムへのAPIコールアウト(APIコールアウト)を行ったり、複雑なビジネスロジックを実行したりする場合、同期的なコンテキストでは限界があります。このような課題を解決するために、Salesforceは Asynchronous Apex (非同期Apex) という強力な仕組みを提供しています。

非同期Apexにはいくつかの種類があります:

  • Future Methods (@future): 最もシンプルな非同期処理。メソッドに @future アノテーションを付与するだけで、バックグラウンドで実行させることができます。ただし、引数としてプリミティブ型しか渡せない、ジョブIDを取得できないなどの制約があります。
  • Batch Apex: 数万から数百万件といった非常に大規模なデータセットを処理するために設計されています。データを小さな「バッチ」に分割して処理するため、ガバナ制限を回避するのに非常に有効です。
  • Scheduled Apex: 特定の時刻にジョブを定期的に実行するための仕組みです。
  • Queueable Apex: 本稿の主役です。Future MethodsのシンプルさとBatch Apexのパワーを兼ね備えた、柔軟で強力な非同期処理の仕組みです。

Queueable Apex は、以下のようなシナリオで特に輝きます:

  • 複雑なデータ型の受け渡し: Future Methodsとは異なり、sObject やカスタムApexクラスのインスタンスをジョブに渡すことができます。これにより、複雑なデータを簡単に非同期処理に引き渡すことが可能になります。
  • ジョブのチェーニング: ある非同期ジョブの完了後、次の非同期ジョブを開始する、といった一連の処理を実装できます。これにより、依存関係のあるタスクを順番に実行できます。
  • ジョブの監視: ジョブをキューに追加(エンキュー)すると、一意のジョブIDが返されます。このIDを使って、AsyncApexJob オブジェクトをクエリし、ジョブのステータス(待機中、処理中、完了、失敗など)を追跡できます。

一言で言えば、Queueable Apexは「少し複雑だがFuture Methodでは力不足、しかしBatch Apexを導入するほど大規模でもない」という、多くの一般的な非同期処理のニーズに応えるための最適なソリューションです。


原理の説明

Queueable Apexの核となるのは、Queueable という名前のシステムインターフェースです。このインターフェースを実装したApexクラスは、非同期実行のためにApexジョブキューに追加できるようになります。

Queueable インターフェースを実装するには、クラスに以下の要素を定義する必要があります:

  1. implements Queueable の宣言: クラス定義で、このインターフェースを実装することを明記します。
  2. execute メソッドの実装: Queueable インターフェースで唯一要求されるメソッドです。このメソッド内に、非同期で実行したいビジネスロジックを記述します。シグネチャは public void execute(QueueableContext context) です。

execute メソッドが受け取る QueueableContext オブジェクトは、現在実行中のジョブに関するコンテキスト情報を提供します。現時点では、このオブジェクトから取得できる主要な情報は getJobId() メソッドによって返される現在のジョブIDのみですが、将来的に拡張される可能性があります。

ジョブの起動方法

Queueable Apexクラスのインスタンスを作成し、それを System.enqueueJob() メソッドに渡すことで、ジョブを非同期実行キューに追加します。

MyQueueableClass job = new MyQueueableClass(someSObjectList, someParameter);
ID jobId = System.enqueueJob(job);

このメソッドは即座に ID 型のジョブIDを返します。このIDは非常に重要で、後からジョブの進行状況を追跡するために使用できます。例えば、以下のようなSOQLクエリでジョブの状態を確認できます。

AsyncApexJob jobInfo = [SELECT Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId];

ジョブのチェーニング (Job Chaining)

Queueable Apexの最も強力な機能の一つがジョブのチェーニングです。これは、あるQueueableジョブの execute メソッド内から、別のQueueableジョブを System.enqueueJob() を使って起動する機能です。これにより、一連の非同期処理を順番に実行するワークフローを構築できます。

重要: 1つの execute メソッド内から呼び出せる System.enqueueJob() は1回だけです。これにより、無限ループやリソースの過剰消費を防いでいます。また、チェーニングの深さにも制限があります(通常、Developer Edition組織では2つ目のジョブまで)。


サンプルコード

以下は、Salesforce公式ドキュメントに掲載されている、取引先(Account)レコードを更新し、その後、別のジョブをチェーンする典型的なQueueable Apexの例です。

最初のジョブ:取引先の親IDを設定する

このクラスは、特定の親取引先IDを持つ取引先を検索し、それらの説明項目を更新します。

// Salesforce Developer Documentationからの公式サンプルコード
public class UpdateParentAccount implements Queueable {
    private ID parentId;
    private List<Account> accounts;

    // コンストラクタ:処理対象のレコードとパラメータを受け取る
    public UpdateParentAccount(List<Account> records, ID id) {
        this.accounts = records;
        this.parentId = id;
    }

    // executeメソッド:非同期で実行されるメインロジック
    public void execute(QueueableContext context) {
        // コンストラクタで渡された取引先リストをループ処理
        for (Account acct : accounts) {
            // 説明項目に親取引先IDを追記
            acct.Description = 'Parent Account ID: ' + parentId;
        }
        // DML操作でデータベースを更新
        update accounts;
        
        // 最初のジョブが完了したら、次のジョブ(AddPrimaryContact)をチェーンする
        // 連絡先を作成し、特定の取引先に紐付ける
        System.enqueueJob(new AddPrimaryContact('New Contact', parentId));
    }
}

チェーンされるジョブ:プライマリ連絡先を追加する

このクラスは、前のジョブから呼び出され、指定された取引先に新しい連絡先を作成します。

// Salesforce Developer Documentationからの公式サンプルコード
public class AddPrimaryContact implements Queueable {
    private Contact contact;
    private ID accountId;
    
    // コンストラクタ
    public AddPrimaryContact(String contactName, ID acctId) {
        this.contact = new Contact(LastName = contactName);
        this.accountId = acctId;
    }
    
    // executeメソッド
    public void execute(QueueableContext context) {
        // 親となる取引先をクエリ
        // 注意:実際のコードでは、accountIdが存在しない場合のエラーハンドリングを追加すべき
        Account account = [SELECT Id FROM Account WHERE Id = :this.accountId LIMIT 1];
        
        // 連絡先に取引先IDを設定
        this.contact.AccountId = account.Id;
        // DML操作で連絡先を挿入
        insert this.contact;
    }
}

ジョブの起動

これらのジョブを起動するには、Anonymous Apexやトリガーなどから最初のジョブをエンキューします。

// 準備:テストデータを作成
Account parent = new Account(Name='Parent Account');
insert parent;

List<Account> children = new List<Account>();
for (Integer i = 0; i < 5; i++) {
    children.add(new Account(Name='Child ' + i, ParentId=parent.Id));
}
insert children;

// 最初のQueueableジョブをエンキュー
ID jobId = System.enqueueJob(new UpdateParentAccount(children, parent.Id));

// ジョブIDを使ってステータスを確認
System.debug('Queueable job ID: ' + jobId);

注意事項

Queueable Apexは非常に便利ですが、使用する際にはいくつかの重要な点を考慮する必要があります。

ガバナ制限

各Queueableジョブは、同期トランザクションとは独立した、独自のガバナ制限セットを持ちます。この制限は同期待ち処理よりも大幅に緩和されています(例:SOQLクエリ100→200、合計ヒープサイズ6MB→12MB)。しかし、無制限ではありません。ジョブ内でもバルク化(bulkification)を意識し、ループ内でのSOQLクエリやDML操作は避けるべきです。

API制限とキューの深さ

1つのトランザクションからエンキューできるジョブの数には制限があります(通常50)。また、ジョブのチェーニングには深さの制限が存在します。本番環境とSandbox環境ではこの制限が異なる場合があるため、設計時には注意が必要です。

テスト

非同期Apexのテストは、同期コードのテストとは少し異なります。Test.startTest()Test.stopTest() を使用することが不可欠です。System.enqueueJob() の呼び出しをこの2つのメソッドで囲むと、Test.stopTest() が実行された時点でキュー内のすべての非同期ジョブが同期的(直列的)に実行されます。これにより、ジョブの実行結果をテストメソッド内で検証(アサーション)できます。

@isTest
private class UpdateParentAccountTest {
    static testMethod void testQueueable() {
        // 1. テストデータの設定
        Account parent = new Account(Name='Test Parent');
        insert parent;
        List<Account> children = new List<Account>{ new Account(Name='Test Child', ParentId=parent.Id) };
        insert children;
        
        // 2. Test.startTest()とTest.stopTest()で非同期処理を囲む
        Test.startTest();
        System.enqueueJob(new UpdateParentAccount(children, parent.Id));
        Test.stopTest();
        
        // 3. 結果の検証(アサーション)
        // stopTest()の実行後にデータベースの状態が更新されていることを確認
        Account updatedChild = [SELECT Description FROM Account WHERE Id = :children[0].Id];
        System.assertEquals('Parent Account ID: ' + parent.Id, updatedChild.Description);
        
        // チェーンされたジョブの結果も検証
        Contact newContact = [SELECT LastName FROM Contact WHERE AccountId = :parent.Id];
        System.assertEquals('New Contact', newContact.LastName);
    }
}

エラー処理

execute メソッド内でハンドルされない例外が発生した場合、ジョブは「失敗」ステータスで終了します。これでは、何が問題だったのかを追跡するのが困難です。堅牢な実装のためには、execute メソッド全体を try-catch ブロックで囲み、エラーが発生した場合にはカスタムオブジェクトやプラットフォームイベントにログを記録するなどのエラーハンドリング戦略を実装することが強く推奨されます。


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

Queueable Apexは、Salesforceプラットフォームにおける非同期処理のための強力で柔軟なツールです。Future Methodの制約を克服し、Batch Apexほどの複雑さを必要としない多くのシナリオで理想的な選択肢となります。

非同期Apexの使い分け

  • Future Method: 外部へのコールアウトなど、単純な「投げっぱなし」の非同期処理で、sObjectを渡す必要がなく、ジョブIDも不要な場合に限定的に使用します。
  • Queueable Apex: ほとんどの非同期処理の第一候補。sObjectの受け渡し、ジョブの監視、ジョブのチェーニングが必要な場合に最適です。
  • Batch Apex: 数万件を超える大規模なデータセットを、分割して確実に処理する必要がある場合に使用します。

ベストプラクティス

  1. 冪等性(Idempotency)を意識する: 可能な限り、ジョブを複数回実行しても結果が変わらないように設計します。これにより、万が一ジョブが再試行された場合でも、データの不整合を防ぐことができます。
  2. ロジックをバルク化する: 非同期コンテキストでも、コードのバルク化は必須です。単一のジョブで処理するデータ量が多くなる可能性を常に念頭に置いてください。
  3. トリガーからの直接チェーニングに注意: トリガーから単一のQueueableジョブを起動するのは一般的ですが、複雑なジョブチェーンを開始すると、デバッグが困難になり、予期せぬ制限に達する可能性があります。トリガーでは処理をキューに入れることに専念し、複雑なワークフローはQueueableジョブ自体に管理させるのが良いでしょう。
  4. 堅牢なエラーハンドリングとロギング: カスタムログオブジェクトやプラットフォームイベントを活用して、ジョブの失敗を確実に捕捉し、調査できるようにします。ジョブが「静かに失敗する」ことだけは避けなければなりません。
  5. Apexジョブキューを監視する: [設定] > [Apexジョブ]ページを定期的に確認し、非同期プロセスの健全性を監視する習慣をつけましょう。

これらの原則とベストプラクティスを遵守することで、Queueable Apexを効果的に活用し、スケーラブルで堅牢なSalesforceアプリケーションを構築することができるでしょう。

コメント