Salesforceにおける非同期Apexの完全マスターガイド:Future、Queueable、Batch Apexを開発者視点で徹底解説

背景と適用シナリオ

Salesforce開発者として、私たちは常にプラットフォームのガバナ制限(Governor Limits)と向き合っています。1つのトランザクション内で実行できるSOQLクエリの数、DMLステートメントの数、CPU処理時間などには厳しい制限が設けられています。ユーザーがボタンをクリックするなどの同期的な処理でこれらの制限を超えてしまうと、ユーザーエクスペリエンスを損なうエラーが発生します。ここで重要になるのがAsynchronous Apex(非同期Apex)です。

非同期Apexは、処理をバックグラウンドで実行するための強力な仕組みです。これにより、時間のかかる処理やリソースを大量に消費する処理を即時実行のトランザクションから切り離し、より高いガバナ制限が適用される別のスレッドで実行できます。これにより、アプリケーションのスケーラビリティとパフォーマンスが大幅に向上します。

主な適用シナリオ:

  • 外部システムへのコールアウト:トリガーやVisualforceコントローラーから直接外部システムのAPIを呼び出すと、応答待機中にユーザーインターフェースがブロックされたり、ガバナ制限に抵触したりする可能性があります。非同期Apexを使えば、これらのコールアウトをバックグラウンドで安全に実行できます。
  • 大規模データ処理:数万件、数百万件のレコードに対して一括で更新や計算を行う場合、同期処理ではまず間違いなくガバナ制限に達します。Batch Apex(バッチApex)を利用すれば、データを小さなチャンクに分割して処理できます。
  • 複雑なビジネスロジックの実行:複数のオブジェクトにまたがる複雑な計算やデータ集計など、CPU時間を大量に消費する処理は、非同期化することでタイムアウトを防ぎます。
  • 定期的なメンテナンス処理:毎日深夜にデータのクレンジングを行ったり、週次でレポート用の集計データを作成したりするなど、定期的に実行する必要があるタスクはSchedulable Apex(スケジュール可能Apex)が最適です。

私たち開発者にとって、非同期Apexを理解し、適切に使い分けることは、堅牢でスケーラブルなSalesforceアプリケーションを構築するための必須スキルと言えるでしょう。


原理説明

非同期Apexは、Salesforceのマルチテナント環境でリソースを公平に分配するためのキューイングフレームワーク上で動作します。開発者が非同期ジョブを投入すると、そのジョブはキューに追加され、プラットフォームのリソースが利用可能になった時点で実行されます。各非同期ジョブは、独自のトランザクションと、より緩やかなガバナ制限を持ちます。ここでは、開発者が利用できる主要な4つの非同期Apexの機能について解説します。

1. Future メソッド (Future Methods)

Future Methodsは、@futureアノテーションを付与した静的メソッドです。最もシンプルに非同期処理を実装する方法であり、「実行したら、あとはお任せ(fire and forget)」というシナリオに適しています。

  • 特徴:
    • メソッドはstatic voidである必要があります。
    • 引数として受け取れるのは、プリミティブデータ型(Integer, Stringなど)またはそのコレクションのみです。sObjectを直接渡すことはできないため、レコードIDのリストなどを渡して、メソッド内で再度クエリする必要があります。
    • 実行の順番は保証されません。複数のFutureメソッドを呼び出しても、それらが呼び出し順に実行されるとは限りません。
    • ジョブのIDが返されないため、実行状況の監視が難しいという側面があります。
    • 外部システムへのコールアウトを行う場合は、@future(callout=true)と指定する必要があります。

2. Queueable Apex (キュー可能Apex)

Queueable Apexは、Futureメソッドの多くの制限を克服した後継機能です。Queueableインターフェースを実装することで利用できます。

  • 特徴:
    • 複雑なデータ型(sObjectなど)を引数として渡すことができます。これにより、メソッド内での再クエリが不要になります。
    • System.enqueueJob(this)を呼び出すとジョブIDが返されるため、AsyncApexJobオブジェクトをクエリしてジョブのステータス(準備中、処理中、完了など)を追跡できます。
    • ジョブの連鎖(Job Chaining)が可能です。1つのQueueableジョブのexecuteメソッド内から、別のQueueableジョブを起動できます。これにより、連続した非同期処理を構築できます。

3. Batch Apex (バッチApex)

Batch Apexは、数千から数百万件といった非常に大規模なデータセットを処理するために設計されています。Database.Batchableインターフェースを実装し、3つのメソッド(start, execute, finish)を定義します。

  • startメソッド:処理対象の全レコードを特定するSOQLクエリを返します。ここで返されたレコードが、後続のexecuteメソッドで処理されるチャンクに分割されます。
  • executeメソッド:startメソッドで取得したレコードを小さなバッチ(デフォルトでは200件)に分けて、バッチごとに繰り返し実行されます。各バッチの実行は、独自のガバナ制限を持つ独立したトランザクションです。
  • finishメソッド:すべてのバッチ処理が完了した後に一度だけ実行されます。処理結果の通知(メール送信など)や、後続処理の起動などに利用されます。

4. Schedulable Apex (スケジュール可能Apex)

Schedulable Apexは、特定の時刻にApexクラスを定期的に実行するための機能です。Schedulableインターフェースを実装し、executeメソッドを定義します。

  • 特徴:
    • 「設定」メニューの「Apexクラス」からUIでスケジュールを設定するか、System.scheduleメソッドを使ってプログラムでスケジュールを設定できます。
    • 日次、週次、月次など、柔軟なスケジュール設定が可能です。
    • 主に、夜間のデータクレンジング、定期的なデータ集計、外部システムとの定期同期などに利用されます。


示例代码

ここでは、Salesforce公式ドキュメントに記載されているコードを基に、各非同期Apexの実装例を詳細なコメント付きで解説します。

1. Future メソッドの例

この例では、取引先(Account)のIDリストを受け取り、関連するすべての取引先責任者(Contact)の姓(LastName)を更新するFutureメソッドを示します。外部コールアウトも想定し、callout=trueを指定しています。

public class SomeFutureClass {
    // @future アノテーションを付与することで、このメソッドが非同期で実行されることを示します。
    // callout=true は、このメソッド内から外部Webサービスへのコールアウトを許可することを示します。
    @future(callout=true)
    public static void updateContactLastName(List<Id> accountIds, String newLastName) {
        
        // 引数で受け取った取引先IDに紐づく取引先責任者をすべて取得します。
        List<Contact> contactsToUpdate = [SELECT Id, LastName FROM Contact WHERE AccountId IN :accountIds];
        
        // 取得した各取引先責任者の姓を新しい姓で更新します。
        for(Contact c : contactsToUpdate) {
            c.LastName = newLastName;
        }
        
        // try-catchブロックでDML操作を囲むことは、エラーハンドリングのベストプラクティスです。
        try {
            // DML操作(レコードの更新)を実行します。
            update contactsToUpdate;
        } catch (Exception e) {
            // エラーが発生した場合、デバッグログにエラーメッセージを出力します。
            // 本番環境では、カスタムオブジェクトへのログ記録など、より堅牢なエラー処理を実装すべきです。
            System.debug('An error has occurred: ' + e.getMessage());
        }
    }
}

2. Queueable Apex の例

この例では、取引先(Account)を更新し、さらに別のジョブをチェーン(連鎖)させるQueueable Apexクラスを示します。

// Queueableインターフェースを実装します。
public class MyQueueableClass implements Queueable {
    
    // このジョブで処理する取引先レコードを保持するメンバー変数。
    private Account accountToProcess;
    
    // コンストラクタで処理対象の取引先を受け取ります。
    public MyQueueableClass(Account acc) {
        this.accountToProcess = acc;
    }

    // Queueableインターフェースで必須のexecuteメソッド。
    // ジョブがキューから取り出されると、このメソッドが実行されます。
    public void execute(QueueableContext context) {
        // 実際のビジネスロジックをここに記述します。
        // この例では、取引先の説明項目を更新しています。
        accountToProcess.Description = 'Updated by Queueable Apex at ' + System.now();
        update accountToProcess;
        
        // ジョブの連鎖(Job Chaining)の例。
        // このジョブが完了した後、次のジョブ(SecondQueueableClass)をキューに追加します。
        // これにより、順序だった非同期処理が可能になります。
        System.enqueueJob(new SecondQueueableClass());
    }
}

// 連鎖される側のQueueableクラスの例
public class SecondQueueableClass implements Queueable {
    public void execute(QueueableContext context) {
        // 2番目のジョブの処理をここに記述します。
        System.debug('Second queueable job executed.');
    }
}

3. Batch Apex の例

この例では、すべての取引先(Account)レコードを取得し、その説明(Description)項目を更新するBatch Apexクラスを示します。

// Database.Batchableインターフェースを実装します。
// <sObject>には、処理対象のオブジェクトを指定します。
public class UpdateAccountDescriptions implements Database.Batchable<sObject> {

    // 1. startメソッド: 処理対象のレコードを決定します。
    // Database.QueryLocatorを返す必要があります。
    public Database.QueryLocator start(Database.BatchableContext bc) {
        // 処理したいすべての取引先レコードを取得するSOQLクエリを返します。
        // Salesforceプラットフォームがこのクエリ結果を自動的に小さなチャンクに分割します。
        String query = 'SELECT Id, Name, Description FROM Account';
        return Database.getQueryLocator(query);
    }

    // 2. executeメソッド: チャンクごとに処理を実行します。
    // scopeには、startメソッドで取得したレコードのリスト(デフォルトで最大200件)が渡されます。
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // 各取引先レコードをループ処理します。
        for (Account acc : scope) {
            acc.Description = 'Updated by Batch Apex on ' + System.today();
        }
        // このスコープ(チャンク)内のレコードを更新します。
        update scope;
    }

    // 3. finishメソッド: すべてのバッチ処理が完了した後に実行されます。
    public void finish(Database.BatchableContext bc) {
        // 処理完了をシステム管理者にメールで通知するなどの後処理を行います。
        AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed,
                           TotalJobItems, CreatedBy.Email
                           FROM AsyncApexJob WHERE Id = :bc.getJobId()];
        
        // ここでメール送信などの処理を実装します。
        System.debug('Batch job finished. Processed items: ' + job.JobItemsProcessed);
    }
}

注意事項

非同期Apexを実装する際には、以下の点に注意が必要です。これらを怠ると、予期せぬエラーやパフォーマンスの低下につながる可能性があります。

ガバナ制限

非同期処理は同期的処理よりも高いガバナ制限を持ちますが、無制限ではありません。例えば、1つのトランザクションから呼び出せるFutureメソッドは50個まで、キューに追加できるQueueableジョブは50個まで(開発者組織では異なります)といった制限があります。また、24時間以内に実行できる非同期Apexの総実行時間やAPIコール数にも上限があります。常に最新のApex Governor Limitsドキュメントを確認することが重要です。

テストクラスの実装

非同期Apexのテストは、通常のApexクラスのテストとは少し異なります。非同期プロセスはテストメソッドの実行完了後に実行されるため、そのままでは結果を検証できません。そこで、Test.startTest()Test.stopTest()ブロックを使用します。

  • Test.startTest():非同期処理を呼び出す前にコールします。これにより、ガバナ制限がリセットされます。
  • 非同期メソッドの呼び出し:Test.startTest()Test.stopTest()の間に、テスト対象の非同期処理(例:System.enqueueJob()Database.executeBatch())を呼び出すコードを記述します。
  • Test.stopTest():このメソッドを呼び出すと、テストコンテキスト内で開始されたすべての非同期ジョブが同期的に実行されます。つまり、stopTest()の次の行に進む前に、非同期処理が完了します。

この仕組みにより、Test.stopTest()の後に、非同期処理によって更新されたであろうレコードの状態をクエリし、System.assertEquals()などでアサーション(結果検証)を行うことができます。

エラーハンドリング

非同期処理はバックグラウンドで実行されるため、エラーが発生してもユーザーには直接通知されません。そのため、堅牢なエラーハンドリング戦略が不可欠です。

  • try-catchブロックを必ず使用し、例外を捕捉します。
  • * 捕捉した例外は、カスタムオブジェクト(ログオブジェクトなど)に記録するか、プラットフォームイベントを発行する、あるいはシステム管理者にメール通知するなどして、エラーを追跡可能にする必要があります。 * Batch Apexでは、Database.Statefulインターフェースを実装することで、バッチの実行をまたいでエラーカウントなどの状態を保持できます。また、Database.RaisesPlatformEventsインターフェースを実装すると、バッチのエラーがプラットフォームイベントとして自動的に発行され、購読して処理することが容易になります。

冪等性(Idempotency)

特に外部システムとの連携などでは、非同期ジョブが何らかの理由(プラットフォームの一時的な問題など)で再試行される可能性があります。そのため、処理は冪等(べきとう)に設計することが望ましいです。冪等性とは、同じ操作を何度繰り返しても、結果が同じになる性質を指します。例えば、レコードを作成する処理であれば、同じリクエストが2回到達した場合でも、重複したレコードが作成されないようにチェックする仕組みを設けるべきです。


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

非同期Apexは、Salesforceプラットフォームの制限を乗り越え、スケーラブルで高性能なアプリケーションを構築するための不可欠なツールです。最後に、どの非同期処理を選択すべきかの指針と、ベストプラクティスをまとめます。

どの非同期ツールを選ぶか?

  • 単純な非同期処理やトリガーからのコールアウトが必要な場合:

    Futureメソッドが手軽で適しています。ただし、sObjectを渡せない、ジョブIDが取れないといった制約を理解した上で使用しましょう。

  • ジョブの実行状況を監視したい、sObjectを渡したい、または処理を連鎖させたい場合:

    Queueable Apexを選択します。これはFutureメソッドの現代的な代替手段であり、ほとんどのケースでより優れた選択肢となります。

  • 大量のレコード(数千件以上)を一括処理する必要がある場合:

    迷わずBatch Apexを使用します。データを自動的に分割し、ガバナ制限を回避しながら安全に処理できます。

  • 毎日、毎週など、決まった時間に処理を実行したい場合:

    Schedulable Apexが最適です。Batch ApexやQueueable Apexを定期的に起動する用途でよく使われます。

ベストプラクティス

  1. 常に一括処理(Bulkification)を意識する:非同期Apex内でも、SOQLクエリやDMLステートメントがループ内にないかなど、基本的な一括処理の原則は遵守してください。
  2. 適切なツールを選択する:上記で概説したように、シナリオに最も適した非同期ツールを選択することが、パフォーマンスと保守性の鍵です。
  3. 堅牢なエラーハンドリングとロギングを実装する:バックグラウンド処理のエラーは見過ごされがちです。必ずエラーを捕捉し、追跡可能な形で記録してください。
  4. テストカバレッジを確保する:Test.startTest()Test.stopTest()を駆使して、非同期ロジックが期待通りに動作することを網羅的にテストしてください。
  5. ガバナ制限を意識する:非同期処理の制限は緩やかですが、無限ではありません。設計段階で、処理が制限内に収まるかを見積もることが重要です。

非同期Apexをマスターすることは、Salesforce開発者としてのスキルを一段階引き上げます。これらのツールを効果的に活用し、ユーザーにとって快適で、信頼性の高いアプリケーションを構築していきましょう。

コメント