Salesforce Queueable Apexをマスターする:複雑なロジックのための非同期処理ガイド

背景と応用シーン

Salesforceプラットフォームでアプリケーションを開発する際、我々開発者が常に意識しなければならないのが、Governor Limits(ガバナ制限)です。これは、プラットフォーム上のリソースをすべてのユーザーが公平に利用できるようにするための、Salesforceによる実行制限です。例えば、1つのトランザクション内で実行できるSOQLクエリの数や、CPUの実行時間には上限が設けられています。

ユーザーの操作(ボタンのクリックなど)を起点とする同期処理では、これらの制限に抵触しやすく、特に大量のデータを処理したり、外部システムとの連携(コールアウト)を行ったりする複雑なビジネスロジックは、実行時エラーを引き起こす原因となり得ます。

この課題を解決するために、SalesforceはAsynchronous Apex(非同期Apex)という仕組みを提供しています。非同期Apexを利用することで、時間のかかる処理をバックグラウンドで実行させ、ユーザーを待たせることなく、より大きなガバナ制限の恩恵を受けることができます。非同期Apexにはいくつかの種類がありますが、本記事ではその中でも特に柔軟性と強力な機能を持つQueueable Apexに焦点を当てて解説します。

Queueable Apexは、以下のようなシーンで非常に有効です。

複雑なデータ処理

単一のレコード更新がトリガーとなり、関連する多数のレコードに対して複雑な計算や更新処理を行う必要がある場合。同期処理ではCPUタイムアウトに陥る可能性がありますが、Queueable Apexを使えば、処理を非同期ジョブとしてキューに追加し、独立したトランザクションで安全に実行できます。

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

Apexトリガーの中から直接外部システムのAPIを呼び出す(コールアウトする)ことはできません。しかし、Queueable Apexを使えば、トリガーからQueueableジョブを起動し、そのジョブ内から安全にコールアウトを実行することが可能です。これにより、Salesforceのデータ変更をリアルタイムに近い形で外部システムに連携できます。

ジョブの連鎖(Job Chaining)

ある非同期処理が完了した後に、その結果を使って別の非同期処理を開始したい場合があります。Queueable Apexは、一つのジョブの中から次のジョブを起動する「ジョブチェーン」を簡単に実装できるという大きな利点があります。これは、よりシンプルな非同期処理であるFutureメソッドにはない強力な機能です。


原理説明

Queueable Apexの核となるのは、Salesforceが提供するQueueableインターフェースです。このインターフェースを実装したApexクラスは、非同期ジョブとしてシステムキューに追加することができます。

Queueableインターフェースには、実装必須のメソッドが一つだけ定義されています。

public interface Queueable {
    void execute(QueueableContext context);
}

execute(QueueableContext context): このメソッド内に、非同期で実行したいビジネスロジックを記述します。ジョブがキューから取り出されて実行される際に、Salesforceプラットフォームがこのメソッドを呼び出します。引数のQueueableContextオブジェクトを通じて、現在実行中のジョブのIDを取得することができます。

Queueable Apexジョブを起動するには、System.enqueueJob()メソッドを使用します。このメソッドは、Queueableインターフェースを実装したクラスのインスタンスを引数に取り、ジョブをキューに追加します。

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

このメソッドは、キューに追加されたジョブのID(AsyncApexJobオブジェクトのID)を返します。このIDを利用して、後からジョブの実行状況(待機中、処理中、完了、失敗など)を追跡することが可能です。

Queueable Apexは、しばしばFutureメソッド(@futureアノテーションを付与したメソッド)と比較されます。両者は非同期処理という点で共通していますが、Queueable Apexには以下のような明確な利点があります。

  • 複雑なデータ型の受け渡し:Futureメソッドの引数はプリミティブ型、プリミティブ型のコレクション、またはその両方の配列に限定されます。一方、Queueable Apexはクラスのメンバー変数としてsObjectなどの複雑なデータ型を保持し、コンストラクタ経由で渡すことができます。
  • ジョブIDの取得:前述の通り、System.enqueueJob()はジョブIDを返すため、処理の追跡が容易です。FutureメソッドではジョブIDを直接取得できません。
  • ジョブチェーン:executeメソッド内からさらに別のQueueableジョブをSystem.enqueueJob()で起動することで、処理を順番に繋げていくことができます。これにより、複雑なワークフローを非同期で構築できます。

サンプルコード

ここでは、Salesforceの公式ドキュメントに記載されているコードを基に、具体的な使用方法を解説します。

基本的なQueueableクラスの実装

この例では、取引先(Account)レコードをいくつか受け取り、その説明(Description)項目を更新するシンプルなQueueableジョブを作成します。

// Queueableインターフェースを実装したクラスを定義
public class UpdateAccountDescriptionQueueable implements Queueable {

    // 処理対象の取引先リストを保持するメンバー変数
    private List<Account> accounts;
    private String description;

    // コンストラクタで処理に必要なデータを受け取る
    public UpdateAccountDescriptionQueueable(List<Account> records, String desc) {
        this.accounts = records;
        this.description = desc;
    }

    // 非同期処理の本体となるexecuteメソッド
    public void execute(QueueableContext context) {
        // 渡された取引先リストをループ処理
        for (Account acct : accounts) {
            // 説明項目を更新
            acct.Description = this.description;
        }
        
        // DML操作を実行
        // Queueableジョブは独自のガバナ制限を持つため、
        // 大量のレコードでも安全に更新できる
        update accounts;
    }
}

このジョブを起動するには、以下のようにApexコードを実行します。

// まず、処理対象となる取引先レコードをSOQLで取得
List<Account> accs = [SELECT Id, Name, Description FROM Account WHERE Name LIKE 'Acme%'];

// 更新したい説明文を定義
String newDesc = 'This is the new description from a Queueable job.';

// Queueableクラスのインスタンスを生成し、データをコンストラクタ経由で渡す
UpdateAccountDescriptionQueueable queueableJob = new UpdateAccountDescriptionQueueable(accs, newDesc);

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

// 取得したジョブIDを使って、後からステータスを確認できる
System.debug('Queueable job ID is: ' + jobId);

ジョブチェーンの実装

次に、一つのジョブが完了した後に、次のジョブを起動するジョブチェーンの例を見てみましょう。この例では、最初のジョブ(親ジョブ)が何らかの処理を行い、その後に2番目のジョブ(子ジョブ)をキューに追加します。

// 1番目に実行される親ジョブ
public class FirstJob implements Queueable {
    public void execute(QueueableContext context) {
        // 何らかの処理を実行
        // 例: 取引先を作成する
        Account a = new Account(Name='Acme Inc.');
        insert a;

        // 処理が完了したら、2番目のジョブをキューに追加する
        // 2番目のジョブに、作成した取引先のIDを渡す
        System.enqueueJob(new SecondJob(a.Id));
    }
}

// 2番目に実行される子ジョブ
public class SecondJob implements Queueable {
    private ID accountId;

    public SecondJob(ID acctId) {
        this.accountId = acctId;
    }

    public void execute(QueueableContext context) {
        // 親ジョブから渡されたIDを使って取引先を取得
        Account acct = [SELECT Name FROM Account WHERE Id = :this.accountId];
        
        // 何らかの追加処理を実行
        // 例: 関連する取引先責任者を作成する
        Contact c = new Contact(
            FirstName = 'Test',
            LastName = 'Contact',
            AccountId = acct.Id
        );
        insert c;
    }
}

このチェーンを開始するには、最初のジョブをキューに追加するだけです。

// FirstJobをキューに追加すると、その処理完了後に自動的にSecondJobがキューに追加される
ID parentJobId = System.enqueueJob(new FirstJob());

注意事項

Queueable Apexは非常に強力ですが、利用する上でいくつか注意すべき点があります。

Governor Limits(ガバナ制限)

Queueableジョブは、起動元のトランザクションとは別の、独立したガバナ制限を持ちます。つまり、CPU時間やヒープサイズ、SOQLクエリ数などがリセットされます。しかし、Queueable Apex自体にもいくつかの制限が存在します。

  • キュー投入の制限: 1つのトランザクション内からSystem.enqueueJob()を呼び出せる回数は、通常1回です(Batch Apexのexecuteメソッド内など一部のコンテキストではこの限りではありません)。
  • 同時実行ジョブ数の上限: 組織全体で同時にキューに追加できる、または実行中の非同期Apexジョブ(Batch, Future, Queueableを含む)の総数には上限があります(通常は100件)。
  • ジョブチェーンの深さ: Developer Editionや本番組織では、1つのジョブからチェーンできる子ジョブは1つだけです。無限ループを防ぐための安全装置です。

コールアウトの許可

Queueableジョブ内から外部システムへHTTPコールアウトを行う場合は、クラスの定義にDatabase.AllowsCalloutsインターフェースを追加で実装する必要があります。

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

テストクラスの実装

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

@isTest
private class UpdateAccountDescriptionQueueableTest {
    @isTest
    static void testQueueable() {
        // 1. テストデータを作成
        List<Account> testAccounts = new List<Account>();
        for (Integer i = 0; i < 5; i++) {
            testAccounts.add(new Account(Name='Test Account ' + i));
        }
        insert testAccounts;

        String testDesc = 'Updated by test.';

        // 2. Test.startTest()とTest.stopTest()で非同期処理を囲む
        Test.startTest();
        // Queueableジョブをキューに追加
        System.enqueueJob(new UpdateAccountDescriptionQueueable(testAccounts, testDesc));
        Test.stopTest();

        // 3. 非同期処理が完了した後の結果を検証
        List<Account> updatedAccounts = [SELECT Description FROM Account WHERE Id IN :testAccounts];
        for (Account acc : updatedAccounts) {
            System.assertEquals(testDesc, acc.Description, 'Description should be updated.');
        }
    }
}

エラーハンドリングと冪等性(Idempotency)

executeメソッド内では、予期せぬエラーが発生する可能性を考慮し、必ずtry-catchブロックを使用して例外を捕捉し、適切に処理(カスタムオブジェクトへのエラーログ記録など)することが推奨されます。また、何らかの理由でジョブが再試行される可能性もゼロではありません。そのため、ジョブのロジックは冪等(Idempotent)、つまり同じ入力で複数回実行されても常に同じ結果になるように設計することが重要です。


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

Queueable Apexは、Salesforceプラットフォームにおける非同期処理の強力な選択肢です。Futureメソッドのシンプルさを超え、Batch Apexほどの重量級処理を必要としない、中規模の非同期タスクに最適です。

以下に、Queueable Apexを効果的に活用するためのベストプラクティスをまとめます。

  • 適切な選択: sObjectなどの複雑なデータを渡したい、ジョブのIDを追跡したい、処理を連鎖させたい場合は、FutureメソッドよりもQueueable Apexを選択します。数万件以上のレコードを処理する場合は、Batch Apexを検討してください。
  • 責務の単一化: 1つのQueueableクラスには、単一の責任(例えば「取引先責任者のステータスを更新する」など)を持たせ、ロジックをシンプルに保ちます。
  • コンストラクタの活用: 処理に必要なデータは、すべてコンストラクタ経由でクラスのメンバー変数に渡します。executeメソッド内でSOQLクエリを発行すると、予期せぬデータ変更の影響を受けたり、ガバナ制限に達しやすくなったりします。
  • 堅牢なエラーハンドリング: executeメソッド全体をtry-catchで囲み、エラーが発生した際には詳細な情報をカスタムオブジェクトなどに記録して、後から追跡できるようにします。
  • 非同期リミットの監視: 大規模な実装を行う際は、組織全体の非同期Apexジョブの制限を常に意識し、「Apexジョブ」の監視画面を定期的に確認します。
  • 徹底したテスト: 想定されるすべてのシナリオ(正常系、異常系、境界値)をカバーするテストクラスを作成し、コードが期待通りに動作することを保証します。

これらの原理とベストプラクティスを理解し、適切に活用することで、Salesforce開発者として、よりスケーラブルで堅牢なアプリケーションを構築することができるでしょう。

コメント