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

背景と適用シナリオ

Salesforce 開発者として、私たちは日常的にプラットフォームのガバナ制限 (Governor Limits) と向き合っています。特に、一度のトランザクションで実行できる CPU 時間、SOQL クエリの数、DML 操作の数には厳しい制限があります。ユーザーが Visualforce ページや Lightning コンポーネントでボタンをクリックしたとき、その裏で実行される処理がこれらの制限を超えてしまうと、ユーザーはエラー画面を目にすることになります。これは、ユーザーエクスペリエンスを著しく損なうだけでなく、ビジネスプロセスの停滞にも繋がりかねません。

このような課題を解決するために Salesforce が提供しているのが、非同期 Apex (Asynchronous Apex) です。非同期 Apex は、処理をバックグラウンドで実行するための強力な仕組みです。これにより、時間のかかる処理やリソースを大量に消費する処理を現在のユーザートランザクションから切り離し、別のスレッドで実行させることができます。

具体的な適用シナリオとしては、以下のようなケースが挙げられます。

大量データの一括処理

何千、何万ものレコードに対して一括で更新や計算を行う場合、同期処理ではほぼ確実にガバナ制限に抵触します。非同期 Apex を使用すれば、これらのレコードを分割してバックグラウンドで安全に処理できます。

外部システムへのコールアウト

外部システムの API を呼び出す処理 (Callout) は、相手方システムの応答時間に依存するため、時間がかかる可能性があります。ユーザーの操作を長時間ブロックしないよう、コールアウト処理を非同期で行うのが一般的です。

複雑なビジネスロジックの実行

複雑な計算や、複数のオブジェクトにまたがる依存関係の強いロジックを実行する場合、CPU 時間を大量に消費することがあります。これらの処理を非同期化することで、ユーザーは処理の完了を待つことなく、次の操作に進むことができます。

非同期 Apex にはいくつかの種類がありますが、本記事ではその中でも特に柔軟性と強力な機能を備えた Queueable Apex に焦点を当て、その原理から実践的な使い方までを詳しく解説していきます。


原理説明

Queueable Apex は、非同期 Apex の実装方法の一つで、Queueable インターフェースを実装することで利用できます。これは、より古くから存在する @future アノテーション(future メソッド)の多くの制限を克服するために設計されました。

Queueable Apex の主な特徴と、future メソッドに対する優位点は以下の通りです。

1. 複雑なデータ型の受け渡し

future メソッドが引数として受け取れるのは、プリミティブデータ型(Integer, String など)やそのコレクションのみです。一方、Queueable Apex は、コンストラクタを介して sObject やカスタム Apex クラスのインスタンスといった、より複雑なデータ型を渡すことができます。これにより、処理に必要な情報をオブジェクトとしてまとめて渡せるため、コードの可読性や保守性が向上します。

2. ジョブ ID の取得

System.enqueueJob() メソッドで Queueable Apex のジョブをキューに追加すると、そのジョブの一意の ID (Job ID) が返されます。この ID を使用して、AsyncApexJob オブジェクトを照会することで、ジョブのステータス(「キュー内」「処理中」「完了」「失敗」など)を追跡・監視することが可能です。これは、処理の進捗をユーザーにフィードバックしたり、エラー発生時に原因を特定したりする上で非常に重要です。future メソッドでは、このようなジョブの追跡はできません。

3. ジョブの連鎖 (Job Chaining)

Queueable Apex の最も強力な機能の一つが、ジョブの連鎖です。ある Queueable ジョブの execute メソッド内から、新しい Queueable ジョブを System.enqueueJob() で起動することができます。これにより、一連の非同期処理を順番に実行するような、より複雑なワークフローを構築できます。例えば、ステップ1でデータを準備し、ステップ2でそのデータを使って外部システムを呼び出し、ステップ3で結果を更新する、といった一連の処理を連結させることが可能です。

Queueable Apex を実装するには、クラスで Queueable インターフェースを実装し、そのインターフェースが要求する唯一のメソッドである execute(QueueableContext context) を定義する必要があります。すべてのロジックは、この execute メソッド内に記述します。

public class MyQueueableClass implements Queueable {
    public void execute(QueueableContext context) {
        // ここに非同期で実行したい処理を記述します
    }
}

外部システムへのコールアウトを行いたい場合は、Database.AllowsCallouts インターフェースも併せて実装する必要があります。

public class MyQueueableWithCallout implements Queueable, Database.AllowsCallouts {
    public void execute(QueueableContext context) {
        // コールアウトを含む非同期処理を記述します
    }
}

示例代码

ここでは、Salesforce の公式ドキュメントで紹介されている、取引先 (Account) レコードを更新し、さらに別のジョブを連鎖させる Queueable Apex のサンプルコードを見ていきましょう。この例では、取引先のリストを受け取り、それぞれの取引先の「取引先責任者の役割 (Contact Role)」から主担当者 (Primary Contact) を見つけ、取引先のカスタム項目を更新します。その後、2番目のジョブをキューに追加します。

Queueable クラスの定義

まず、取引先の主担当者情報を更新するメインの Queueable クラスを作成します。

// AddPrimaryContact クラスは、Queueable インターフェースを実装します。
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) にあるすべての取引先を取得します。
        // WHERE 句で絞り込むことで、不要なデータをロードしないようにします。
        List<Account> accounts = [SELECT Id, Name, Primary_Contact__c 
                                 FROM Account 
                                 WHERE BillingState = :state];
        
        // 取得した取引先のリストをループ処理します。
        for (Account acc : accounts) {
            // カスタム項目 'Primary_Contact__c' にコンストラクタで受け取った
            // 取引先責任者の ID を設定します。
            acc.Primary_Contact__c = contact.Id;
        }
        
        // DML 操作はループの外で一括実行するのがベストプラクティスです。
        // これにより、ガバナ制限 (DML statements) の消費を抑えられます。
        update accounts;

        // --- ジョブの連鎖 (Job Chaining) ---
        // このジョブが完了した後、別のジョブ (SecondJob) をキューに追加します。
        // これにより、処理を段階的に実行できます。
        System.enqueueJob(new SecondJob());
    }
}

連鎖させるジョブの定義

次に、上記のジョブから連鎖して実行される2番目のジョブクラスを定義します。この例ではシンプルなものにしています。

// 連鎖される側のジョブも同様に Queueable インターフェースを実装します。
public class SecondJob implements Queueable {
    public void execute(QueueableContext context) {
        // 実際にはここで別の処理(例:完了通知の送信、ログの記録など)を行います。
        // この例ではシンプルにデバッグログを出力するだけに留めます。
        System.debug('Second job executed.');
    }
}

ジョブの起動

これらの Queueable ジョブは、トリガー、Apex クラス、または匿名実行ウィンドウから起動できます。

// 匿名実行ウィンドウからジョブを起動する例

// まず、主担当者として設定する取引先責任者レコードを作成または取得します。
// ここではサンプルとして新しいレコードを作成しています。
Contact testContact = new Contact(FirstName='Jane', LastName='Doe');
insert testContact;

// 州を指定します。ここでは 'CA' (カリフォルニア州) を対象とします。
String state = 'CA';

// Queueable クラスのインスタンスを作成し、コンストラクタに必要なデータを渡します。
AddPrimaryContact job = new AddPrimaryContact(testContact, state);

// System.enqueueJob() を呼び出してジョブをキューに追加します。
// 戻り値として Job ID を受け取ることができます。
ID jobId = System.enqueueJob(job);

// Job ID を使ってジョブのステータスを確認できます。
System.debug('Queueable job started with ID: ' + jobId);

注意事项

ガバナ制限 (Governor Limits)

非同期 Apex も独自のガバナ制限を持ちます。同期処理よりも制限は緩和されていますが、無限ではありません。

  • キュー投入制限: 1つのトランザクション内で System.enqueueJob() を呼び出せるのは、最大50回までです。
  • 24時間の実行回数: 組織全体で24時間あたりに実行できる非同期 Apex (future, queueable, batch 含む) の合計回数には上限があります(組織のエディションによりますが、通常は250,000回またはライセンス数の1000倍のいずれか大きい方)。
  • ジョブの連鎖: execute メソッド内から System.enqueueJob() でキューに追加できるジョブは1つだけです。無限ループを防ぐための制約です。

エラー処理 (Error Handling)

execute メソッド内で捕捉されない例外が発生した場合、そのジョブは失敗し、ステータスが "Failed" になります。重要なビジネスロジックを実行する場合は、必ず try-catch ブロックを使用して例外を捕捉し、エラーハンドリングを行うべきです。エラーの内容をカスタムオブジェクトに記録したり、管理者に通知メールを送信したりするなどの処理を実装することが推奨されます。

public void execute(QueueableContext context) {
    try {
        // 主要な処理
    } catch (Exception e) {
        // エラーログをカスタムオブジェクトに保存する処理
        // 管理者への通知処理など
        System.debug('An error occurred: ' + e.getMessage());
    }
}

テスト (Testing)

Queueable Apex のテストは、Test.startTest()Test.stopTest() を使用して行います。この2つのメソッドの間に System.enqueueJob() の呼び出しを記述します。Test.stopTest() が実行されると、キューに追加された非同期処理が同期的に実行されます。これにより、非同期処理が完了した後の状態をアサーション (System.assertEquals など) で検証することができます。

@isTest
private class AddPrimaryContactTest {
    @isTest
    static void testQueueableLogic() {
        // --- 準備 (Setup) ---
        // テスト用のデータを作成します。
        Contact testContact = new Contact(FirstName='Test', LastName='Contact');
        insert testContact;
        
        Account acc = new Account(Name='Test Account', BillingState='NY');
        insert acc;

        // --- 実行 (Execute) ---
        Test.startTest();
        // Queueable ジョブをキューに追加します。
        AddPrimaryContact job = new AddPrimaryContact(testContact, 'NY');
        System.enqueueJob(job);
        Test.stopTest(); // ここで非同期処理が同期的に実行されます。

        // --- 検証 (Assert) ---
        // 非同期処理の結果を検証します。
        Account updatedAccount = [SELECT Primary_Contact__c FROM Account WHERE Id = :acc.Id];
        System.assertEquals(testContact.Id, updatedAccount.Primary_Contact__c, 'Primary contact should be updated.');
    }
}

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

Queueable Apex は、Salesforce プラットフォーム上で複雑な非同期処理を実装するための非常に強力で柔軟なツールです。sObject などの複雑なデータ型を扱え、ジョブ ID で処理を追跡し、ジョブを連鎖させることができるため、多くのシナリオで future メソッドよりも優れた選択肢となります。

ベストプラクティス

  • 冪等性 (Idempotency) の確保: 非同期処理は、何らかの理由で再試行される可能性があります。処理が複数回実行されても問題が発生しないように、冪等性を意識した設計を心がけてください。
  • 一括処理 (Bulkification): execute メソッド内では、常に複数のレコードを処理できるように設計してください。一度に1レコードしか処理しないコードは、パフォーマンスの低下やガバナ制限の超過に繋がります。
  • 適切な非同期ツールの選択:
    • 単純な「撃ちっぱなし」の非同期処理や、トリガーからコールアウトを行いたい場合は future メソッド が適している場合があります。
    • ジョブの監視や連鎖、複雑なデータ型の受け渡しが必要な場合は Queueable Apex を選択します。
    • 数万件を超える非常に大規模なデータセットを処理する場合は、専用のフレームワークである Batch Apex を使用します。
    • 特定の時間に定期的に処理を実行したい場合は Schedulable Apex を使用します。
  • 堅牢なエラーハンドリング: 本番環境で安定して稼働させるためには、予期せぬエラーを適切に処理し、追跡できる仕組みが不可欠です。

Salesforce 開発者として、これらの非同期処理の選択肢を正しく理解し、シナリオに応じて最適なツールを選択する能力は、スケーラブルで堅牢なアプリケーションを構築するための鍵となります。

コメント