Salesforce Futureメソッド:非同期Apex処理ガイド


背景と適用シナリオ

Salesforceプラットフォームは、マルチテナント環境で動作しており、すべてのユーザーが公平にリソースを共有できるように、厳格なガバナ制限 (Governor Limits) が設けられています。同期処理、例えばトリガーやVisualforceページのコントローラー内での処理は、CPU時間、ヒープサイズ、DML操作の回数など、様々な制限に直面します。特に、同期トランザクション内から外部サービスのAPIを呼び出す(コールアウトする)ことは、レスポンス時間が予測できないため、原則として許可されていません。

このような制約を回避し、より高度でリソースを消費する処理を実現するために、Salesforceは非同期処理の仕組みを提供しています。その中でも、Futureメソッド (Future Methods) は、最もシンプルで広く利用されている非同期Apexの実装方法の一つです。

Futureメソッドの主な適用シナリオは以下の通りです。

  • トリガーからのWebサービスコールアウト: レコードが作成または更新された際に、トリガーから外部システムにデータを連携する必要がある場合。同期トリガーからは直接コールアウトできないため、Futureメソッドを使用して非同期にコールアウトを実行します。
  • リソースを大量に消費する処理の分離: 複雑な計算や多数のレコードに対するDML操作など、同期処理のガバナ制限に抵触する可能性のある重い処理をバックグラウンドで実行させたい場合。
  • Mixed DMLエラーの回避: 同一トランザクション内で、ユーザーやプロファイルなどの設定オブジェクト (Setup Object) と、取引先や商談などの非設定オブジェクト (Non-Setup Object) の両方に対してDML操作を行うと、Mixed DMLエラーが発生します。Futureメソッドを使って処理を別のトランザクションに分離することで、このエラーを回避できます。

Futureメソッドは、現在のトランザクションから処理を切り離し、Salesforceがリソースに余裕のある時に実行するためのキューに投入する「Fire and Forget」モデルに適しています。


原理の説明

Futureメソッドの核心は、Apexクラスのメソッドに付与される @future アノテーションです。このアノテーションが付けられたメソッドは、呼び出されるとすぐには実行されません。代わりに、非同期実行キューに追加され、Salesforceプラットフォームがリソースを確保できたタイミングで、独立した新しいトランザクションとして実行されます。

Futureメソッドの定義

Futureメソッドには、以下の厳格なルールがあります。

  • メソッドは必ず static として定義する必要があります。
  • 戻り値の型は必ず void でなければなりません。
  • メソッドが受け取れる引数の型は、プリミティブデータ型 (Integer, String, Booleanなど)、プリミティブデータ型の配列、またはプリミティブデータ型のコレクション (List<ID>, Set<String> など) に限定されます。

パラメータの制約について

Futureメソッドの引数にsObjectやApexクラスのオブジェクトを渡すことはできません。これは、Futureメソッドが呼び出された時点と、実際に実行される時点とでは時間的な隔たりがあるためです。その間にsObjectのデータが変更されている可能性があり、データの不整合を防ぐための制約です。この制約を回避する一般的な方法は、sObjectのIDのリストを渡し、Futureメソッドの内部でSOQLを使って最新のデータを再取得することです。

コールアウトの許可

Futureメソッドから外部Webサービスへのコールアウトを行う場合は、アノテーションに (callout=true) パラメータを追加する必要があります。これにより、Salesforceプラットフォームに対して、この非同期処理が外部通信を行うことを明示します。

例: @future(callout=true)

実行キューとトランザクション

@future アノテーションが付いたメソッドを呼び出すと、そのリクエストは非同期処理キューに追加されます。Salesforceはキューを監視し、システムリソースが利用可能になると、メソッドを順次実行します。各Futureメソッドの実行は、独自のガバナ制限を持つ完全に新しいトランザクションとして扱われます。これにより、呼び出し元の同期トランザクションの制限とは独立して処理を行うことができます。


サンプルコード

ここでは、取引先 (Account) レコードが作成された際に、その情報を外部サービスに送信するという典型的なシナリオのサンプルコードを示します。このコードはSalesforce公式ドキュメントに基づいています。

ステップ1: Futureメソッドを持つApexクラスの作成

まず、コールアウトロジックを含むFutureメソッドを定義したクラスを作成します。このメソッドは取引先IDのリストを引数として受け取り、内部でデータを再クエリしてから外部サービスへ送信します。

// AccountProcessor.cls
public class AccountProcessor {
    
    // @future アノテーションを付与し、コールアウトを許可する
    @future(callout=true)
    public static void sendAccountInfo(Set<Id> accountIds) {
        
        // IDのセットから関連する取引先情報をSOQLで取得する
        // これにより、メソッド実行時の最新のデータで処理を行うことができる
        List<Account> accounts = [SELECT Id, Name, Phone, Website FROM Account WHERE Id IN :accountIds];
        
        // 外部サービスに送信するデータ(ここではJSON形式)を構築する
        // 実際には、複数の取引先情報をリストとしてシリアライズすることが多い
        String jsonPayload = JSON.serialize(accounts);

        // HttpRequestをセットアップする
        HttpRequest request = new HttpRequest();
        // 実際のエンドポイントURLに置き換える必要がある
        request.setEndpoint('https://api.example.com/accounts'); 
        request.setMethod('POST');
        request.setHeader('Content-Type', 'application/json;charset=UTF-8');
        request.setHeader('Authorization', 'Bearer YOUR_API_KEY'); // 認証情報を追加
        request.setBody(jsonPayload);
        
        Http http = new Http();
        try {
            // コールアウトを実行
            HttpResponse response = http.send(request);
            
            // レスポンスをチェックし、成功したかどうかを判断する
            if (response.getStatusCode() == 201) {
                // 成功ログを記録する(カスタムオブジェクトやプラットフォームイベントを使用)
                System.debug('Successfully sent account info for ' + accounts.size() + ' accounts.');
            } else {
                // エラーレスポンスをログに記録する
                System.debug('Callout failed. Status: ' + response.getStatus() + ', Status Code: ' + response.getStatusCode());
                System.debug('Response Body: ' + response.getBody());
                // ここでカスタムのエラーハンドリング処理を実装する
            }
        } catch(System.CalloutException e) {
            // コールアウト例外を捕捉し、ログに記録する
            System.debug('Callout error: '+ e.getMessage());
        }
    }
}

ステップ2: トリガーからFutureメソッドを呼び出す

次に、取引先が作成された後 (after insert) に上記のFutureメソッドを呼び出すトリガーを作成します。

// AccountTrigger.trigger
trigger AccountTrigger on Account (after insert) {

    // Trigger.new から作成された取引先のIDを収集する
    // Set を使用することで、重複するIDを自動的に排除できる
    Set<Id> newAccountIds = Trigger.newMap.keySet();

    // IDのセットが空でないことを確認してからFutureメソッドを呼び出す
    if (!newAccountIds.isEmpty()) {
        // Futureメソッドを呼び出す。この呼び出しはすぐにキューに追加され、
        // トリガーのトランザクションはここでFutureメソッドの完了を待たずに終了する
        AccountProcessor.sendAccountInfo(newAccountIds);
    }
}

この構成により、トリガーはFutureメソッドの呼び出しをキューイングするだけで迅速に完了し、ユーザーエクスペリエンスを損なうことなく、重いコールアウト処理をバックグラウンドで安全に実行できます。


注意事項

Futureメソッドは非常に便利ですが、その特性を理解し、いくつかの重要な点に注意する必要があります。

ガバナ制限

Futureメソッド自体も非同期処理のガバナ制限に従います。

  • 呼び出し回数: 1つのApexトランザクション内から呼び出せるFutureメソッドは最大50回までです。
  • 24時間の制限:組織全体で24時間以内に実行できる非同期Apex (Future, Queueable, Batch) の総数は、ライセンスによって異なりますが、一般的には250,000回またはユーザーライセンス数×200回のうち、多い方が上限となります。

実行順序の非保証

複数のFutureメソッドを連続して呼び出したとしても、それらが呼び出された順序で実行される保証はありません。Salesforceのキューイングメカニズムに依存するため、実行順序が重要なビジネスロジックには不向きです。順序性を担保したい場合は、Queueable Apex の連鎖 (Chaining) 機能を検討してください。

テスト方法

Futureメソッドを含むコードをテストするには、Test.startTest()Test.stopTest() を使用します。テストメソッド内でFutureメソッドを呼び出した後、Test.stopTest() を実行すると、それまでにキューイングされた非同期処理が同期的に実行されます。これにより、Futureメソッドの実行結果をテストクラス内で検証 (Assert) することができます。

@isTest
private class AccountProcessorTest {
    @isTest
    static void testSendAccountInfo() {
        // テスト用のモックHTTPレスポンスを設定
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());

        // テストデータを作成
        List<Account> accs = new List<Account>();
        for (Integer i = 0; i < 5; i++) {
            accs.add(new Account(Name = 'Test Account ' + i));
        }
        insert accs;
        
        // テストのコンテキストを開始
        Test.startTest();
        
        // トリガーが起動し、Futureメソッドがキューに入る
        // この時点ではまだ実行されていない
        Account updatedAccount = [SELECT Id, Name FROM Account WHERE Name = 'Test Account 0' LIMIT 1];
        updatedAccount.Name = 'Updated Test Account';
        update updatedAccount; // トリガーを再度起動させるような処理を実行(例)
        
        // テストのコンテキストを停止
        // このメソッドが呼ばれると、キュー内のすべての非同期処理が実行される
        Test.stopTest();
        
        // ここでFutureメソッドの実行結果を検証する
        // 例えば、コールアウトが成功したことを示すカスタムログオブジェクトが
        // 作成されたかどうかなどを確認する
        // Integer calloutLogs = [SELECT COUNT() FROM CalloutLog__c];
        // System.assertEquals(1, calloutLogs, 'A callout log should have been created.');
    }
}

エラーハンドリング

Futureメソッドは別のトランザクションで実行されるため、内部で例外が発生しても、呼び出し元のトランザクションにエラーを返すことはできません。そのため、Futureメソッド内での堅牢なエラーハンドリングが不可欠です。try-catchブロックを使用して例外を捕捉し、エラー内容をカスタムオブジェクトやプラットフォームイベントに記録して、管理者が後から追跡できるように設計することが推奨されます。


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

Futureメソッドは、Salesforceにおける非同期処理の基本的なツールであり、特定のシナリオで非常に効果的です。

Futureメソッドの利用が適しているケース:

  • トリガーや同期プロセスから外部サービスへのコールアウトを行いたい場合。
  • 処理を現在のトランザクションから切り離し、バックグラウンドで独立して実行させたい場合。
  • Mixed DMLエラーを回避したい場合。
  • 処理の順序性や、実行後の状態監視が不要な、比較的シンプルな非同期タスク。

Futureメソッドの利用を避けるべきケース:

  • 大量のレコード(数千、数万件以上)を処理する必要がある場合。この場合はBatch Apexが最適です。
  • 複数の非同期ジョブを連鎖させて、順番に処理を実行したい場合。この場合はQueueable Apexを使用します。
  • 非同期処理のジョブIDを取得して、その後の状態を監視したい場合。Queueable ApexはジョブIDを返すため、より高度な監視が可能です。
  • sObjectのような複雑なデータ型を引数として渡したい場合。Queueable ApexはsObjectを直接渡すことができます。

ベストプラクティス

  1. ロジックをまとめる: Futureメソッドは、特定のタスクを実行するための単一のロジックコンテナとして設計します。トリガー内にはロジックを記述せず、IDを渡してFutureメソッドを呼び出すだけにします。
  2. バルク対応: Futureメソッドは、常に複数のレコードを処理できるように、引数をリストやセットで受け取るように設計します。これにより、ガバナ制限内で効率的に動作します。
  3. べき等性 (Idempotency) を意識する: 稀なケースですが、非同期ジョブが再試行される可能性があります。ロジックは、複数回実行されても問題が発生しないように設計することが望ましいです。
  4. 適切なツールを選択する: Futureメソッドは強力ですが、万能ではありません。より複雑な要件(ジョブの連鎖、状態監視、大量データ処理)には、Queueable ApexやBatch Apexといった、より高度な非同期ツールを選択することを常に検討してください。特に、新規開発においては、Futureメソッドよりも柔軟性の高いQueueable Apexが第一候補となることが多いです。

Futureメソッドの原理と制約を正しく理解し、適切なシナリオで活用することで、Salesforceアプリケーションのスケーラビリティとパフォーマンスを大幅に向上させることができます。

コメント