背景と適用シナリオ
Salesforce 開発者として、私たちは日常的にデータの操作に取り組んでいます。しかし、数千、数百万のレコードを一度に処理する必要がある場合、Salesforce の同期処理における Governor Limits (ガバナ制限) という壁に直面します。例えば、1 回のトランザクションで発行できる SOQL クエリは 100 回まで、DML 操作の対象レコード数は 10,000 件までといった制限です。これらの制限は、プラットフォーム全体のパフォーマンスと安定性を保つために非常に重要ですが、大規模なデータクレンジング、データ移行、または全顧客に対する一括更新といったタスクの実行を困難にします。
ここで登場するのが Batch Apex (バッチ Apex) です。Batch Apex は、Salesforce が提供する非同期処理フレームワークの一つであり、大量のレコードを小さなバッチ (チャンク) に分割して処理することを可能にします。これにより、同期処理のガバナ制限を回避し、長時間かかる可能性のある処理をバックグラウンドで安定して実行できます。
具体的な適用シナリオとしては、以下のようなものが考えられます。
- データクレンジング: 全ての取引先レコードの住所を標準化したり、古いリードレコードをアーカイブしたりする。
- データ移行・更新: 外部システムからインポートした大量のデータに対して、関連レコードを作成・更新する。
- 複雑な計算処理: 全ての商談レコードを対象に、複雑なロジックを用いて売上予測を再計算し、カスタム項目に結果を保存する。
- 定期的なメンテナンス: 毎晩、特定の条件を満たすケースを自動的にクローズする。
このように、Batch Apex は開発者が大規模データセットを扱う際の強力な武器となります。本記事では、Salesforce 開発者の視点から Batch Apex の原理を掘り下げ、具体的な実装方法、注意点、そしてベストプラクティスについて詳しく解説します。
原理の説明
Batch Apex の中核をなすのは Database.Batchable というインターフェースです。このインターフェースを Apex クラスに実装 (implement) することで、そのクラスはバッチ処理クラスとして機能します。Database.Batchable インターフェースは、以下の3つのメソッドを実装することを要求します。
1. start メソッド
Database.QueryLocator start(Database.BatchableContext bc)
このメソッドは、バッチ処理ジョブの開始時に一度だけ呼び出されます。その役割は、処理対象となる全てのレコードを収集し、フレームワークに渡すことです。戻り値は Database.QueryLocator または Iterable オブジェクトです。
- Database.QueryLocator: SOQL クエリの結果を返します。最大で 5,000 万件のレコードを取得できるため、非常に大きなデータセットを扱う場合に最適です。SOQL のガバナ制限は、この
QueryLocatorのクエリに対しては適用されず、効率的に対象レコードを特定できます。開発者としては、これが最も一般的に使用する選択肢です。 - Iterable: カスタムのイテレーションロジックを提供したい場合や、SOQL で直接取得できないデータ (例: 外部APIから取得したデータ) を処理する場合に使用します。
2. execute メソッド
void execute(Database.BatchableContext bc, List<SObject> scope)
このメソッドがバッチ処理の心臓部です。start メソッドで取得されたレコードは、指定されたバッチサイズ (デフォルトは200) のチャンクに分割され、そのチャンクごとに execute メソッドが呼び出されます。引数の scope には、処理対象となるレコードのリストが渡されます。
各 execute メソッドの実行は、独立したトランザクションとして扱われ、それぞれに独自のガバナ制限が適用されます。これにより、全体の処理が一度にガバナ制限に達することなく、大規模なデータセットを安全に処理できます。例えば、100万件のレコードをバッチサイズ200で処理する場合、execute メソッドは 5,000回呼び出されることになります。
3. finish メソッド
void finish(Database.BatchableContext bc)
全てのバッチ処理が完了した後、この finish メソッドが一度だけ呼び出されます。後処理タスクを実行するのに最適な場所です。例えば、処理結果をまとめたメールを管理者に送信したり、別のバッチジョブや非同期処理を開始 (ジョブの連鎖) したりするなどの処理をここで行います。
また、バッチクラスの状態を維持したい場合は、Database.Stateful インターフェースを追加で実装します。これを実装すると、メンバ変数の値が各 execute メソッドのトランザクションをまたいで保持されるようになります。例えば、処理したレコードの総数やエラーが発生したレコードの ID を集計する際に便利です。ただし、シリアライズのオーバーヘッドが発生するため、パフォーマンスへの影響を考慮し、必要な場合にのみ使用することが推奨されます。
示例代码
ここでは、Salesforce の公式ドキュメントにある一般的なユースケースを基に、取引先 (Account) の住所が更新された際に、その取引先に関連する全ての取引先責任者 (Contact) の郵送先住所も更新する Batch Apex のサンプルコードを紹介します。
global class UpdateContactAddresses implements Database.Batchable<sObject> {
// start メソッド: 処理対象のレコードを SOQL クエリで取得します。
// ここでは、特定の取引先に関連する全ての取引先責任者を対象としています。
// Database.QueryLocator を使用することで、最大5,000万件のレコードを効率的に処理できます。
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT ID, MailingStreet, MailingCity, MailingState, MailingPostalCode, ' +
'Account.ShippingStreet, Account.ShippingCity, Account.ShippingState, Account.ShippingPostalCode ' +
'FROM Contact ' +
'WHERE Account.ShippingStreet != null AND ' +
'Account.ShippingCity != null AND ' +
'Account.ShippingState != null AND ' +
'Account.ShippingPostalCode != null'
);
}
// execute メソッド: レコードのチャンクごとに呼び出され、実際の処理ロジックを実行します。
// scope パラメータには、処理対象の取引先責任者レコードのリストが渡されます。
global void execute(Database.BatchableContext bc, List<Contact> scope){
// 更新対象の取引先責任者リストを初期化します。
List<Contact> contactsToUpdate = new List<Contact>();
// scope 内の各取引先責任者レコードをループ処理します。
for (Contact contact : scope) {
// 取引先責任者の郵送先住所を、関連する取引先の配送先住所で上書きします。
contact.MailingStreet = contact.Account.ShippingStreet;
contact.MailingCity = contact.Account.ShippingCity;
contact.MailingState = contact.Account.ShippingState;
contact.MailingPostalCode = contact.Account.ShippingPostalCode;
// 更新リストに追加します。
contactsToUpdate.add(contact);
}
// リストに更新対象のレコードが存在する場合のみ、DML 操作 (update) を実行します。
// これにより、不要な DML を避けることができます。
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
// finish メソッド: 全てのバッチ処理が完了した後に一度だけ呼び出されます。
// ここでは、ジョブのステータスを確認し、完了通知メールを送信します。
global void finish(Database.BatchableContext bc){
// AsyncApexJob オブジェクトを使用して、このバッチジョブの情報を取得します。
AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed,
TotalJobItems, CreatedBy.Email
FROM AsyncApexJob
WHERE Id = :bc.getJobId()];
// メール送信の準備をします。
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
String[] toAddresses = new String[] {job.CreatedBy.Email};
mail.setToAddresses(toAddresses);
mail.setSubject('Contact Address Update Batch Job Status: ' + job.Status);
mail.setPlainTextBody(
'The batch Apex job processed ' + job.TotalJobItems +
' batches with '+ job.NumberOfErrors + ' failures.\n' +
'Processed ' + job.JobItemsProcessed + ' records.'
);
// メールを送信します。
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
このバッチクラスを実行するには、開発者コンソールの Anonymous Apex ウィンドウで以下のコードを実行します。
Id batchJobId = Database.executeBatch(new UpdateContactAddresses(), 200);
Database.executeBatch メソッドの第二引数は、オプションの scopeSize (バッチサイズ) です。この例では 200 を指定していますが、省略した場合はデフォルトの 200 が使用されます。
注意事項
Batch Apex は強力ですが、効果的に利用するためにはいくつかの注意点を理解しておく必要があります。
ガバナ制限
Batch Apex は非同期処理用のより寛容なガバナ制限セット内で実行されますが、制限が全くないわけではありません。重要なのは、ガバナ制限は execute メソッドの各実行 (トランザクション) に対してリセットされるという点です。例えば、1回の execute の実行内で発行できる SOQL は 100 回、DML 行数は 50,000 行までです。バッチサイズを大きくしすぎると、1回の execute 内で処理するレコード数が多くなり、ループ内での SOQL や DML が原因でガバナ制限に達する可能性があります。
バッチサイズ (Batch Size)
バッチサイズは Database.executeBatch メソッドの第二引数で指定でき、1 から 2,000 までの値を設定できます。適切なバッチサイズは、execute メソッド内の処理の複雑さに依存します。
- 小さいバッチサイズ (例: 10-50):
executeメソッドの処理が非常に複雑で、CPU 時間やヒープサイズを多く消費する場合に適しています。ガバナ制限に達するリスクは低いですが、バッチの実行回数が増えるため、全体の処理時間は長くなる可能性があります。 - 大きいバッチサイズ (例: 2000): 処理が単純な DML 操作のみの場合などに適しています。API コールアウトを伴う場合、コールアウトの総数を抑えるために大きなバッチサイズが有効なこともあります。ただし、1トランザクションあたりのリソース消費量が増えるため、ガバナ制限に注意が必要です。
最適なサイズを見つけるには、実際のデータに近い Sandbox 環境でテストとチューニングを繰り返すことが不可欠です。
外部サービスへのコールアウト
Batch Apex から外部サービスの API を呼び出す (コールアウトする) 場合は、クラス定義に Database.AllowsCallouts インターフェースを実装する必要があります。
global class MyBatchClass implements Database.Batchable<sObject>, Database.AllowsCallouts { ... }
エラーハンドリング
execute メソッド内でエラーが発生すると、デフォルトではそのバッチ全体のトランザクションがロールバックされます。しかし、特定のレコードのエラーが他のレコードの処理を妨げないようにしたい場合も多いでしょう。その場合は、try-catch ブロックを使用して例外を捕捉したり、Database.update(records, false) のように DML 操作の第二引数に false を指定して部分的な成功を許容したりする戦略が有効です。これにより、失敗したレコードの情報を収集し、finish メソッドで通知や再処理のキューイングを行うことができます。
テストクラス
Batch Apex のテストは非常に重要です。テストクラスでは、Test.startTest() と Test.stopTest() の間に Database.executeBatch を呼び出します。Test.stopTest() が実行されると、キューに入れられたバッチジョブが同期的に実行されるため、結果をアサーションで検証できます。start、execute、finish の各メソッドが意図通りに動作することを必ず確認してください。
まとめとベストプラクティス
Batch Apex は、Salesforce プラットフォーム上で大規模なデータセットを扱う開発者にとって不可欠なツールです。同期処理のガバナ制限を回避し、リソースを効率的に使用して、安定したデータ処理を実現します。
最後に、Batch Apex を実装する上でのベストプラクティスをまとめます。
startメソッドのクエリは選択的に:WHERE句を効果的に使用し、本当に必要なレコードのみを処理対象とします。不要なデータを読み込むことは、パフォーマンスの低下につながります。executeメソッドのロジックは効率的に: ループ内での SOQL クエリや DML 操作 (SOQL/DML in a loop) は絶対に避けてください。一括処理 (Bulkification) の原則に従い、処理対象の ID を収集してから一度にクエリや DML を実行します。Database.Statefulは慎重に: メンバ変数の状態を保持する必要がある場合にのみ使用します。大規模なデータをメンバ変数に保持すると、ビュー ステートと同様にパフォーマンスが劣化する可能性があります。- バッチサイズのチューニング: 処理内容に応じてバッチサイズを調整し、パフォーマンスとガバナ制限のバランスを取ります。本番環境にデプロイする前に、必ず負荷テストを行ってください。
- 堅牢なエラーハンドリングとロギング: 予期せぬエラーが発生してもジョブ全体が停止しないように、堅牢なエラーハンドリングを実装します。失敗したレコードやエラー内容はカスタムオブジェクトなどに記録し、後で追跡できるようにすることが推奨されます。
- ジョブの連鎖は慎重に:
finishメソッドから次のバッチジョブを呼び出すことは可能ですが、無限ループに陥らないように終了条件を明確に設計する必要があります。
これらの原理とベストプラクティスを理解し、適切に適用することで、Salesforce 開発者としてよりスケーラブルで信頼性の高いソリューションを構築することができるでしょう。
コメント
コメントを投稿