背景と応用シナリオ
Salesforce 開発者として、私たちは常にプラットフォームの Governor Limits (ガバナ制限) と呼ばれる実行制限の中でソリューションを構築する必要があります。同期処理、例えばトリガーや Visualforce のコントローラー内では、CPU 時間、SOQL クエリ数、DML 操作数などに厳しい制限が課せられています。これらの制限を超えると、ユーザーはエラーに直面し、ビジネスプロセスは中断されてしまいます。
特に、以下のようなシナリオで開発者は課題に直面します。
1. 外部システムへのコールアウト
トリガーのコンテキスト内から直接、外部システムの API を呼び出す (Callout (コールアウト)) ことはできません。これは、Salesforce がデータベーストランザクションの整合性を保つためです。外部システムの応答が遅れた場合、データベースのロックが長時間保持され、パフォーマンスに深刻な影響を与える可能性があるため、この操作はブロックされています。しかし、レコードが作成・更新された直後に外部システムへ通知したいという要件は非常によくあります。
2. リソースを大量に消費する処理
複雑な計算、多数の関連レコードの処理、あるいは複数の DML 操作を伴うロジックは、同期処理の CPU 時間制限や DML 制限に抵触する可能性があります。例えば、商談が成立した際に、関連するすべての商談品目に対して複雑な割引計算を適用し、さらにカスタムオブジェクトに詳細なログを記録するような処理は、同期実行の限界を超えることがあります。
これらの課題を解決するために Salesforce が提供しているのが Asynchronous Apex (非同期 Apex) です。Future メソッドは、非同期 Apex の中でも最もシンプルで手軽に利用できる機能の一つです。Future メソッドを利用することで、時間のかかる処理や外部へのコールアウトをバックグラウンドで実行させ、現在のトランザクションから切り離すことができます。これにより、ユーザーは即座に応答を受け取り、システムはリソースが利用可能になった時点で処理を実行するため、パフォーマンスとユーザーエクスペリエンスが向上します。
原理の説明
Future メソッドは、その名の通り「未来」で実行される Apex メソッドです。メソッドに @future アノテーションを付与するだけで、そのメソッドは非同期実行の対象となります。
開発者が @future アノテーションが付いたメソッドを呼び出すと、Salesforce はそのリクエストをキューに追加します。この時点ではメソッドはまだ実行されません。呼び出し元の同期トランザクションは、キューへの追加が完了した時点で直ちに終了します。その後、Salesforce プラットフォームはサーバーリソースの状況を監視し、リソースに空きができた最適なタイミングでキューからリクエストを取り出し、メソッドを実行します。
この仕組みの重要なポイントは以下の通りです。
- 独立したトランザクション: Future メソッドは、呼び出し元の同期処理とは全く別のトランザクションで実行されます。これにより、それぞれが独立したガバナ制限を持つことになります。Future メソッドは、同期処理よりも多くの SOQL クエリ (100 -> 200)、より長い CPU 時間 (10秒 -> 60秒) など、緩和されたガバナ制限の恩恵を受けられます。
- パラメータの制約: Future メソッドに渡せる引数の型には制限があります。渡せるのは、プリミティブデータ型 (Integer, String, Double など)、プリミティブデータ型のコレクション (
List<Id>,Set<String>など)、またはその配列のみです。sObject (標準オブジェクトやカスタムオブジェクト) を直接引数として渡すことはできません。これは、メソッドがキューに入ってから実際に実行されるまでの間に、元の sObject のデータが変更されてしまう可能性があるためです。この不整合を防ぐため、一般的にはレコードの ID を渡し、Future メソッド内で再度 SOQL を使って最新のデータを取得するのがベストプラクティスです。 - 実行順序の非保証: 複数の Future メソッドを連続して呼び出したとしても、それらが呼び出された順序で実行される保証はありません。システムリソースの状況によって実行順序は変動する可能性があるため、処理の順序性が重要なシナリオには不向きです。
- コールアウトの許可: 外部システムへのコールアウトを行う Future メソッドには、
@future(callout=true)のようにアノテーションにパラメータを追加する必要があります。これにより、Salesforce プラットフォームに対して、このメソッドが外部通信を行うことを明示的に伝えます。
示例代码
ここでは、Salesforce の公式ドキュメントで紹介されている典型的なユースケース、つまり取引先 (Account) レコードが更新された際に、トリガーから Future メソッドを呼び出して外部の Web サービスに情報を送信する例を見ていきましょう。
1. Future メソッドを定義した Apex クラス
このクラスは、外部サービスへのコールアウトを実行するロジックを含んでいます。
public class FutureMethodExample {
// @future アノテーションを付与し、callout=true を指定して外部コールアウトを許可します。
// メソッドは static でなければならず、戻り値は void である必要があります。
// 引数にはプリミティブ型のリスト(この場合は String)を使用しています。
@future(callout=true)
public static void sendNotification(String accountId, String accountName, String accountBillingState) {
// 本来はここに外部APIへのコールアウト処理を実装します。
// これは REST API への POST リクエストのダミー実装です。
HttpRequest request = new HttpRequest();
// 外部サービスのエンドポイントURLを設定
request.setEndpoint('https://api.example.com/notifications');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
// 送信するJSONボディを構築
String jsonBody = '{"accountId":"' + accountId + '", "name":"' + accountName + '", "state":"' + accountBillingState + '"}';
request.setBody(jsonBody);
// Http クラスのインスタンスを作成し、リクエストを送信
Http http = new Http();
try {
HttpResponse response = http.send(request);
// 応答をチェックし、成功したかどうかを判断
if (response.getStatusCode() == 200) {
// 成功ログなどを記録(例: System.debug)
System.debug('Successfully sent notification for Account ID: ' + accountId);
} else {
// エラーログを記録
System.debug('Failed to send notification. Status code: ' + response.getStatusCode() + ' Body: ' + response.getBody());
}
} catch (System.CalloutException e) {
// コールアウト例外の処理
System.debug('Callout error: '+ e.getMessage());
}
}
}
2. Future メソッドを呼び出すトリガー
取引先が更新されたときに、上記の Future メソッドを呼び出すトリガーです。
trigger AccountTrigger on Account (after update) {
// トリガーが実行された後に Future メソッドを呼び出します
for (Account acc : Trigger.new) {
// 古い値と新しい値で請求先都道府県が変更された場合のみ実行
if (Trigger.oldMap.get(acc.Id).BillingState != acc.BillingState) {
// Future メソッドを呼び出します。
// sObject を直接渡すのではなく、必要なフィールドの値をプリミティブ型として渡します。
FutureMethodExample.sendNotification(acc.Id, acc.Name, acc.BillingState);
}
}
}
3. Future メソッドのテストクラス
非同期処理をテストするには、Test.startTest() と Test.stopTest() を使用します。Test.stopTest() が実行されると、キューに入れられたすべての非同期処理が同期的に実行され、結果を検証できるようになります。
@isTest
private class FutureMethodExampleTest {
@isTest
static void testSendNotification() {
// テスト用のデータを作成
Account testAccount = new Account(Name='Test Account', BillingState='CA');
insert testAccount;
// 外部サービスへのコールアウトをモックします。
// これにより、実際に外部APIを呼び出すことなくテストが可能になります。
Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
// Test.startTest() と Test.stopTest() の間で非同期処理を呼び出します
Test.startTest();
// トリガーを発火させるためにレコードを更新します
testAccount.BillingState = 'NY';
update testAccount;
// Test.stopTest() を呼び出すと、キュー内の Future メソッドが実行されます。
Test.stopTest();
// ここで、非同期処理が正しく実行されたことを検証するアサーションを記述します。
// 例えば、特定のログが記録されたか、カスタムオブジェクトにレコードが作成されたかなどを確認します。
// この例では、デバッグログが出力されることを確認するだけですが、
// 実際のプロジェクトではより具体的な検証が必要です。
// (デバッグログの直接的なアサートはできないため、ロジックの結果を検証します)
}
// HTTPコールアウトのモック応答を生成するヘルパークラス
public class MockHttpResponseGenerator implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setBody('{"status":"success"}');
res.setStatusCode(200);
return res;
}
}
}
注意事項
Future メソッドは非常に便利ですが、その特性を理解し、いくつかの注意点を守る必要があります。
パラメータの制限
前述の通り、sObject を引数として渡すことはできません。必ずレコードの ID (または他のプリミティブ値) を渡し、メソッド内で必要なデータを再クエリしてください。これにより、常に最新のデータで処理を行うことが保証されます。
ガバナ制限
Future メソッドは独立した高いガバナ制限を持ちますが、無制限ではありません。また、Future メソッドの呼び出し自体にも制限があります。1つの Apex トランザクションから呼び出せる Future メソッドは最大50回までです。また、24時間以内に実行できる Future メソッドの数にも組織ごとの上限があります (組織のエディションによって異なります)。大量の非同期処理が必要な場合は、Batch Apex の利用を検討してください。
エラー処理
Future メソッドはバックグラウンドで実行されるため、例外が発生してもユーザーに直接通知されません。エラーが発生した場合、管理者にメールで通知されますが、それだけでは不十分な場合が多いです。堅牢なエラー処理メカニズムを実装することが重要です。例えば、カスタムオブジェクトを作成してエラーログを記録したり、Platform Events を使用してエラーを通知するなどの方法が考えられます。try-catch ブロックで例外を捕捉し、適切なログ記録を行うことを徹底してください。
監視
Future メソッドの実行状況は、[設定] > [環境] > [ジョブ] > [Apex ジョブ] ページで監視できます。ここでは、キューに入っているジョブ、処理中のジョブ、完了したジョブ、失敗したジョブのステータスを確認できます。デバッグ時にはこのページが非常に役立ちます。
まとめとベストプラクティス
Future メソッドは、Salesforce 開発者が非同期処理を実装するための強力でシンプルなツールです。特に、トリガーからの外部コールアウトや、リソースを中程度に消費する処理を同期トランザクションから切り離したい場合に最適です。
以下に、Future メソッドを効果的に使用するためのベストプラクティスをまとめます。
- 一括処理 (Bulkification) を意識する: Future メソッドは、単一のレコード ID ではなく、ID のリスト (
List<Id>やSet<Id>) を受け取るように設計してください。これにより、一度の呼び出しで複数のレコードを効率的に処理でき、Future メソッドの呼び出し回数制限に達するのを防ぎます。 - べき等性 (Idempotency) を確保する: ネットワークの問題などで、ごく稀に非同期処理が再試行される可能性があります。メソッドが複数回実行されても、結果が同じになるように設計する (べき等性を持たせる) ことが望ましいです。
- 適切なツールを選択する: Future メソッドはシンプルですが、万能ではありません。より複雑な要件には、他の非同期ツールを検討してください。
- 処理の連鎖 (Chaining) や sObject を渡す必要がある場合: Queueable Apex を使用します。Queueable Apex は次のジョブをキューに追加することができ、複雑なデータ型も渡せます。
- 数万〜数百万件の大量のレコードを処理する場合: Batch Apex を使用します。Batch Apex はデータを小さなチャンクに分割して処理するため、大規模データセットに最適です。
- 定期的にジョブを実行する必要がある場合: Schedulable Apex を使用します。
- テストを徹底する:
Test.startTest()とTest.stopTest()を活用し、非同期ロジックが期待通りに動作することを必ずテストでカバーしてください。コールアウトを含む場合は、HttpCalloutMockインターフェースを使用して外部サービスをモック化することが不可欠です。
これらの原則を理解し、適切に Future メソッドを活用することで、よりスケーラブルでパフォーマンスの高い、堅牢な Salesforce アプリケーションを構築することができるでしょう。
コメント
コメントを投稿