Salesforce Apex @future メソッド開発者ガイド:非同期処理をマスターする

背景と適用シナリオ

Salesforce プラットフォームで開発を行う上で、governor limits(ガバナ制限)は避けて通れない重要な概念です。これは、すべてのユーザーが共有リソースを公平に利用できるように Salesforce が設けている実行制限です。例えば、1つのトランザクション内で実行できる SOQL クエリの数や DML ステートメントの数、CPU の合計実行時間などが厳しく制限されています。ユーザーがボタンをクリックしたり、レコードを保存したりする同期的な処理の中で、これらの制限を超えてしまうと、処理はエラーとなり中断されてしまいます。

このような課題を解決するための強力なソリューションが、Asynchronous Apex(非同期 Apex)です。非同期 Apex を利用すると、時間のかかる処理やリソースを大量に消費する処理をバックグラウンドで実行させることができます。これにより、ユーザーは即座に応答を受け取り、アプリケーションの体感速度を向上させることができるだけでなく、同期処理とは別のガバナ制限が適用されるため、より大規模な処理を実行することが可能になります。

今回解説する future method(フューチャーメソッド)は、非同期 Apex の中でも最もシンプルで手軽に利用できる方法の一つです。具体的には、以下のようなシナリオで非常に有効です。

1. 外部システムへの Web サービスコールアウト

Apex トリガーなどの同期的なコンテキストから、外部の Web サービスを直接呼び出す(コールアウトする)ことはできません。これは、外部システムの応答遅延が Salesforce のトランザクション全体に影響を与えるのを防ぐためです。@future アノテーションを付けたメソッドを利用すれば、コールアウト処理を非同期に実行し、この制限を回避できます。

2. リソースを大量に消費する処理の実行

多数のレコードに対する複雑な計算や、大規模なデータ処理は、CPU 実行時間のガバナ制限に抵触する可能性があります。このような処理を future method に移すことで、ユーザーの操作を妨げることなく、バックグラウンドで安全に処理を完了させることができます。

3. DML 操作の分離

Salesforce には、「mixed DML operation」エラーというものがあります。これは、同じトランザクション内でユーザ(User)やグループ(Group)のような設定オブジェクト(Setup Object)と、取引先(Account)や商談(Opportunity)のような非設定オブジェクト(Non-setup Object)の両方に対して DML 操作(挿入、更新、削除)を行おうとすると発生します。future method を使って一方の DML 操作を別のトランザクションに分離することで、このエラーを回避できます。


原理説明

future method の仕組みは非常にシンプルです。特定の Apex メソッドに @future というアノテーション(Annotation)を付与するだけで、そのメソッドは非同期実行の対象となります。

同期的なコードから @future メソッドが呼び出されると、Salesforce プラットフォームはそのメソッドの実行リクエストをキューに追加します。そして、システムの他のリクエストと共に、リソースが利用可能になった時点でそのメソッドを実行します。重要なのは、future method は呼び出し元のトランザクションとは完全に独立した新しいトランザクションで実行されるという点です。これにより、独自のガバナ制限が適用され、同期処理の制限とは切り離して考えることができます。

future method を定義するには、以下のルールに従う必要があります。

  • メソッドは必ず static でなければなりません。
  • メソッドの戻り値は必ず void でなければなりません。
  • メソッドが受け取れる引数(パラメータ)は、primitive data types(プリミティブデータ型 - Integer, String, Boolean など)、プリミティブデータ型の配列、またはプリミティブデータ型のコレクション(List<ID>, Set<String> など)のみです。

特に重要な制約として、sObject を直接引数として渡すことはできません。これは、future method がキューに追加されてから実際に実行されるまでの間に、元の sObject レコードがデータベース上で変更されている可能性があるためです。もし古い状態の sObject を渡してしまうと、意図しないデータ不整合を引き起こす可能性があります。この問題を避けるため、sObject の Id を渡し、future method の中でその Id を使って最新のデータを再クエリ(SOQLで再取得)するのがベストプラクティスです。

また、外部システムへのコールアウトを行う future method の場合は、アノテーションを @future(callout=true) のように指定する必要があります。これにより、Salesforce プラットフォームは、このメソッドが外部へのコールアウトを行うことを認識し、適切なリソースを割り当てます。


示例代码

ここでは、最も一般的なユースケースである「トリガーから外部 Web サービスを非同期で呼び出す」例を見ていきましょう。このコードは Salesforce の公式ドキュメントに基づいています。

以下の FutureMethodExample クラスは、取引先(Account)の ID リストを受け取り、各取引先に関連する情報を外部サービスに送信する(という想定の)コールアウト処理を行います。

Apex クラス: FutureMethodExample

public class FutureMethodExample {

    // @future(callout=true) アノテーションを付与することで、
    // このメソッドが外部 Web サービスへのコールアウトを許可された非同期メソッドであることを示します。
    @future(callout=true)
    public static void sendAccountInfo(List<Id> accountIds) {

        // 引数として受け取った取引先 ID を使用して、必要な項目を含む最新のレコードデータを取得します。
        // sObject を直接引数にせず、ID を元に再クエリするのがベストプラクティスです。
        List<Account> accounts = [SELECT Id, Name, Phone FROM Account WHERE Id IN :accountIds];

        // 外部サービスに送信するデータを JSON 形式で構築します。
        // ここでは例として、取引先名と電話番号を文字列として連結しています。
        String jsonBody = '{"accounts": [';
        for (Integer i = 0; i < accounts.size(); i++) {
            Account acc = accounts.get(i);
            jsonBody += '{';
            jsonBody += '"name": "' + acc.Name + '",';
            jsonBody += '"phone": "' + acc.Phone + '"';
            jsonBody += '}';
            if (i < accounts.size() - 1) {
                jsonBody += ',';
            }
        }
        jsonBody += ']}';

        // HttpRequest オブジェクトをインスタンス化し、コールアウトの設定を行います。
        HttpRequest request = new HttpRequest();
        // 外部サービスのエンドポイントURLを設定します。
        // 実際には、カスタム設定や名前付き認証情報に保存することが推奨されます。
        request.setEndpoint('https://api.example.com/accounts');
        // HTTP メソッドを 'POST' に設定します。
        request.setMethod('POST');
        // ヘッダーに Content-Type を設定します。
        request.setHeader('Content-Type', 'application/json;charset=UTF-8');
        // リクエストボディに作成した JSON 文字列を設定します。
        request.setBody(jsonBody);

        Http http = new Http();
        try {
            // コールアウトを実行します。
            HttpResponse response = http.send(request);

            // 応答のステータスコードを確認します。
            if (response.getStatusCode() == 200) {
                // 成功した場合の処理を記述します。
                // 例えば、成功ログをカスタムオブジェクトに保存するなど。
                System.debug('Callout successful. Response: ' + response.getBody());
            } else {
                // 失敗した場合の処理を記述します。
                // エラーログを保存し、管理者へ通知するなどの処理が考えられます。
                System.debug('Callout failed. Status: ' + response.getStatus() + ', Status Code: ' + response.getStatusCode());
            }
        } catch (System.CalloutException e) {
            // コールアウト例外が発生した場合の処理を記述します。
            // ネットワークエラーなどが考えられます。
            System.debug('Callout error: ' + e.getMessage());
        }
    }
}

トリガー: AccountTrigger

以下のトリガーは、取引先が新規作成または更新された後に、上記の sendAccountInfo メソッドを呼び出します。

trigger AccountTrigger on Account (after insert, after update) {
    // future method に渡すための取引先 ID のリストを初期化します。
    List<Id> accountIds = new List<Id>();

    // トリガーのコンテキスト変数 Trigger.new に含まれるすべての取引先レコードをループ処理します。
    for (Account acc : Trigger.new) {
        // 各取引先の ID をリストに追加します。
        accountIds.add(acc.Id);
    }

    // ID のリストが空でないことを確認してから future method を呼び出します。
    // これにより、不必要な呼び出しを防ぎます。
    if (!accountIds.isEmpty()) {
        // future method を呼び出します。この呼び出しはすぐにキューに追加され、
        // トリガーのトランザクションはここで終了します。
        FutureMethodExample.sendAccountInfo(accountIds);
    }
}

注意事項

future method は非常に便利ですが、利用する上でいくつかの重要な制限と注意点があります。

ガバナ制限

future method にも独自のガバナ制限があります。1つの Apex トランザクションから呼び出せる future method の数は最大50回です。また、24時間以内に実行できる future method の総数にも上限があります(組織のエディションにより異なりますが、一般的には250,000回またはライセンス数の10倍のいずれか大きい方)。大量のレコード更新が頻繁に発生するシナリオでは、この上限に達しないよう設計に注意が必要です。

実行順序の非保証

複数の future method を連続して呼び出したとしても、それらが呼び出された順序で実行される保証はありません。システムのリソース状況に応じて並列で実行されることもあれば、順序が逆転することもあります。もし、処理の実行順序が重要な場合は、future method ではなく Queueable Apex の利用を検討してください。Queueable Apex では、ジョブから次のジョブをチェーン(連結)させることが可能です。

メソッドの連鎖不可

future method の中から、別の future method を呼び出すことはできません。これを行おうとすると、実行時エラーが発生します。

テスト方法

future method を含むコードをテストするには、特別な考慮が必要です。テストメソッド内で future method を呼び出した後、その非同期処理が完了するのを待つ必要があります。これは、テストコードを Test.startTest()Test.stopTest() で囲むことで実現できます。Test.stopTest() が実行されると、それまでにキューに追加されたすべての非同期処理が同期的に実行されます。これにより、非同期処理の結果をテストメソッド内で検証(アサーション)することができます。

エラーハンドリング

future method はバックグラウンドで実行されるため、もし例外が発生しても、そのエラーは UI 上のユーザーには通知されません。エラーが発生したこと自体に気づけない可能性があります。そのため、future method 内では必ず try-catch ブロックを使用して例外を捕捉し、カスタムオブジェクトにエラーログを記録したり、システム管理者にメールで通知したりするなど、堅牢なエラーハンドリング戦略を実装することが不可欠です。


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

future method は、Salesforce における非同期処理の入門として非常に優れた機能です。特に、トリガーからの Web サービスコールアウトや、比較的単純な重い処理をバックグラウンドに移行する際に、最小限のコードで大きな効果を発揮します。

最後に、future method を効果的かつ安全に利用するためのベストプラクティスをまとめます。

  1. sObject ではなく ID を渡す:
    前述の通り、データ整合性を保つために、メソッドの引数には sObject のレコード ID を渡し、メソッド内で最新のデータを再クエリしてください。
  2. 一括処理(Bulkification)を意識する:
    メソッドは単一の ID ではなく、ID のリスト(List<Id>)を受け取るように設計してください。これにより、トリガーが一度に複数のレコードを処理した場合でも、future method の呼び出しは1回で済み、ガバナ制限の消費を抑えることができます。
  3. 冪等性(Idempotency)を考慮する:
    まれに、Salesforce プラットフォームが future method の実行をリトライすることがあります。メソッドが複数回実行されても問題が発生しないように、処理が冪等(何回実行しても結果が同じ)になるように設計することが望ましいです。
  4. 適切なツールを選択する:
    future method はシンプルさが魅力ですが、万能ではありません。ジョブのIDを取得してステータスを監視したい、複雑なデータ型を渡したい、ジョブを連鎖させたい、といったより高度な要件がある場合は、Queueable Apex を検討すべきです。さらに大規模なデータセットをバッチ処理する必要がある場合は、Batch Apex が最適な選択肢となります。
  5. 堅牢なエラーハンドリングとログ記録:
    バックグラウンド処理の失敗を見逃さないよう、必ず例外処理を実装し、何がいつ、なぜ失敗したのかを追跡できる仕組みを構築してください。

これらの原則を守ることで、future method を活用して、よりスケーラブルで応答性の高い Salesforce アプリケーションを構築することができるでしょう。

コメント