Salesforce バッチ Apex 入門:大規模データを効率的に処理するための実践ガイド

背景と適用シナリオ

Salesforce プラットフォームで開発を行う際、開発者は常に Governor Limits (ガバナ制限) という実行制限を意識する必要があります。これは、マルチテナント環境である Salesforce が、すべてのユーザーに安定したパフォーマンスを提供するための重要な仕組みです。例えば、1つのトランザクション内で発行できる SOQL (Salesforce Object Query Language) クエリは100回まで、処理できる DML (Data Manipulation Language) ステートメントは150回まで、CPU実行時間は10秒までといった制限があります。

数件から数百件のレコードを処理する通常の同期処理では、これらの制限が問題になることは稀です。しかし、数万、数十万、あるいはそれ以上の大量のレコードに対して一括でデータクレンジング、項目更新、データ移行、または複雑な集計処理を行いたい場合、同期処理の枠組みでは容易にガバナ制限に抵触してしまいます。

このような課題を解決するために Salesforce が提供しているのが、Asynchronous Apex (非同期 Apex) の一つである Batch Apex (バッチ Apex) です。Batch Apex を使用すると、大量のレコードを「チャンク」と呼ばれる小さなバッチに分割し、それぞれのバッチを独立したトランザクションとして処理できます。これにより、1つの大きな処理を複数の小さな処理に分散させ、ガバナ制限を回避しながら大規模なデータ操作を安全かつ効率的に実行することが可能になります。

主な適用シナリオ:

  • データクレンジング:全顧客データの名寄せや住所の正規化など、大量のレコードを更新する処理。
  • データ移行・アーカイブ:古いデータを別のオブジェクトや外部システムに移動、または不要なデータを削除する処理。
  • 大規模な集計処理:全商談レコードを対象に年度ごとの売上を再計算し、カスタム集計オブジェクトに結果を保存するような複雑なバッチ計算。
  • 外部システム連携:夜間に外部システムから取得した大量のデータを Salesforce に一括で登録・更新する処理。

原理説明

Batch Apex の中核は、Salesforce が提供する Database.Batchable インターフェースです。このインターフェースを実装した Apex クラスを作成することで、バッチ処理のロジックを定義します。Database.Batchable インターフェースには、以下の3つのメソッドを実装する必要があります。

1. start メソッド

start(Database.BatchableContext bc)

このメソッドは、バッチ処理の開始時に一度だけ呼び出されます。その役割は、処理対象となるすべてのレコードを収集し、Salesforce プラットフォームに渡すことです。戻り値は Database.QueryLocator または Iterable<sObject> のいずれかです。

  • Database.QueryLocator: シンプルな SOQL クエリで対象レコードを取得する場合に使用します。最大5,000万件のレコードを取得できるため、非常に大規模なデータセットを扱う場合に最適です。SOQL クエリは、このメソッドが返すオブジェクトに含まれる形で実行されます。これが最も一般的で推奨される方法です。
  • Iterable<sObject>: 外部 Web サービスからデータを取得したり、複雑なロジックで処理対象のリストを動的に生成したりする場合に使用します。柔軟性が高い反面、一度にメモリに保持できるデータ量には限りがあるため、ヒープサイズのガバナ制限に注意が必要です。

2. execute メソッド

execute(Database.BatchableContext bc, List<sObject> scope)

このメソッドがバッチ処理の心臓部です。start メソッドが収集したレコードは、指定されたバッチサイズ(デフォルトは200件)のチャンクに分割され、そのチャンクごとに execute メソッドが呼び出されます。第2引数の List<sObject> scope には、そのチャンクで処理すべきレコードのリストが渡されます。

最も重要な点は、この execute メソッドの呼び出し一回ごとに、新しいガバナ制限のセットが適用されることです。 例えば、10,000件のレコードをバッチサイズ200で処理する場合、execute メソッドは50回呼び出されます。そして、その50回それぞれで、最大100回の SOQL クエリや150回の DML ステートメントが利用可能になります。これにより、全体としてガバナ制限を大幅に超える処理が可能になります。

3. finish メソッド

finish(Database.BatchableContext bc)

すべてのバッチ(チャンク)の処理が完了した後に、一度だけ呼び出されるメソッドです。処理結果のサマリーメールを送信したり、処理状況をカスタムオブジェクトに記録したり、あるいは後続の別のバッチ処理を起動したりといった、後処理を実装するために使用されます。


バッチジョブの実行は、Database.executeBatch メソッドを使って行います。このメソッドは、バッチクラスのインスタンスと、オプションでバッチサイズ(チャンクあたりのレコード数)を引数に取ります。

Id batchJobId = Database.executeBatch(new MyBatchableClass(), 200);

上記のコードは、MyBatchableClass をバッチサイズ200で実行します。実行後、ジョブは非同期処理のキューに追加され、リソースが利用可能になり次第、Salesforce プラットフォームによって実行されます。


示例コード

ここでは、Salesforce の公式ドキュメントでよく使用される、すべてのアカウントレコードの項目を更新する簡単なバッチ処理を例に挙げます。この例では、`Description` 項目に `Batch Processed` という文字列を追加します。

バッチ Apex クラス

以下は、Database.Batchable インターフェースを実装したクラスの例です。

// UpdateAccountFields.apxc
// このバッチクラスは、すべてのアカウントレコードを検索し、Description 項目を更新します。
public class UpdateAccountFields implements Database.Batchable<sObject> {

    // start メソッド:処理対象のレコードを定義します。
    // ここでは、すべてのアカウントの ID と Description を取得する SOQL クエリを返します。
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, Description FROM Account'
        );
    }

    // execute メソッド:各バッチで実行される実際の処理を記述します。
    // scope には、start メソッドで取得したレコードがチャンクに分割されて渡されます。
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // 処理対象の Account レコードのリストをループ処理します。
        for (Account acc : scope) {
            // Description 項目を更新します。
            acc.Description = 'Batch Processed';
        }
        // 変更されたレコードのリストを一括で更新します (DML 操作)。
        // これにより、ガバナ制限の消費を最小限に抑えます。
        update scope;
    }

    // finish メソッド:すべてのバッチ処理が完了した後に実行されます。
    // ここでは、ジョブのステータスを確認するための非同期 Apex ジョブ ID を使ってメールを送信します。
    public void finish(Database.BatchableContext bc) {
        // ジョブの情報を取得
        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('Account Update Batch Job Status: ' + job.Status);
        mail.setPlainTextBody(
            'The batch Apex job processed ' + job.TotalJobItems +
            ' batches with '+ job.NumberOfErrors + ' failures.\n' +
            'Processed items: ' + job.JobItemsProcessed
        );
        // メールを送信
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
}

バッチの実行

このバッチクラスを実行するには、開発者コンソールの匿名実行ウィンドウ(Anonymous Window)で以下のコードを実行します。

// バッチクラスのインスタンスを生成し、Database.executeBatch を呼び出します。
// 第2引数でバッチサイズを指定できます。省略した場合のデフォルトは200です。
Id batchId = Database.executeBatch(new UpdateAccountFields(), 100);
System.debug('Submitted Batch Job ID: ' + batchId);

実行後、[設定] > [ジョブ] > [Apex ジョブ] で処理の進捗状況を確認できます。


注意事項

Batch Apex を使用する際には、いくつかの重要な点に注意する必要があります。

1. ガバナ制限

前述の通り、各 execute メソッドの実行には独立したガバナ制限が適用されます。しかし、バッチサイズが大きすぎたり、execute メソッド内のロジックが非常に複雑だったりすると、そのトランザクション内でガバナ制限(特にCPU時間)に抵触する可能性があります。処理内容に応じて、Database.executeBatch の第2引数で適切なバッチサイズを調整することが重要です。一般的に、DML 操作や SOQL が多い場合は小さめのバッチサイズが、単純な項目更新の場合は大きめのバッチサイズが適しています。

2. 状態の保持 (Stateful)

デフォルトでは、Batch Apex クラスは stateless です。これは、各 execute メソッドの実行間でクラスのインスタンス変数が維持されないことを意味します。例えば、処理したレコードの総数をカウントするような変数を定義しても、execute が呼び出されるたびにリセットされてしまいます。

複数のトランザクションにまたがって状態を維持したい場合は、クラスの定義に Database.Stateful インターフェースを追加する必要があります。これにより、インスタンス変数の値がバッチ処理全体を通じて保持されるようになります。

public class MyStatefulBatch implements Database.Batchable<sObject>, Database.Stateful {
    // この変数は、execute メソッドの呼び出し間で値が保持されます。
    public Integer recordsProcessed = 0;

    public Database.QueryLocator start(Database.BatchableContext bc) {
        // ...
    }

    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // ...
        recordsProcessed = recordsProcessed + scope.size();
    }

    public void finish(Database.BatchableContext bc) {
        System.debug('Total records processed: ' + recordsProcessed);
    }
}

ただし、Database.Stateful を使用すると、オブジェクトの状態をシリアライズするオーバーヘッドが発生するため、パフォーマンスに若干影響を与える可能性があります。必要な場合にのみ使用してください。

3. エラー処理

execute メソッド内で DML 操作を行う際、特定のレコードでエラーが発生しても、バッチ全体を停止させたくない場合があります。そのような場合は、try-catch ブロックを使用したり、DML ステートメントの `allOrNone` パラメータを `false` に設定したりすることで、部分的な成功を許容できます。

// allOrNone パラメータを false に設定することで、一部のレコードが失敗しても
// 成功したレコードはコミットされます。
Database.SaveResult[] saveResults = Database.update(scope, false);

// SaveResult をループして、エラーが発生したレコードを特定し、ログに記録します。
for (Database.SaveResult sr : saveResults) {
    if (!sr.isSuccess()) {
        for(Database.Error err : sr.getErrors()) {
            System.debug('Error updating record ID: ' + sr.getId());
            System.debug('Error message: ' + err.getStatusCode() + ': ' + err.getMessage());
        }
    }
}

4. テストクラス

Batch Apex のテストは非常に重要です。テストコードでは、Test.startTest()Test.stopTest() を使用して非同期処理を同期的に実行させ、結果を検証します。Test.stopTest() が呼び出されると、その前に実行された Database.executeBatch が完了し、結果のアサーションが可能になります。

@isTest
private class UpdateAccountFieldsTest {
    @testSetup
    static void setup() {
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 250; i++) {
            accounts.add(new Account(Name = 'Test Account ' + i));
        }
        insert accounts;
    }

    @isTest
    static void testBatch() {
        Test.startTest();
        // バッチジョブを実行
        Database.executeBatch(new UpdateAccountFields());
        Test.stopTest();

        // バッチ処理後の結果を検証
        List<Account> updatedAccounts = [SELECT Id, Description FROM Account WHERE Name LIKE 'Test Account%'];
        for (Account acc : updatedAccounts) {
            System.assertEquals('Batch Processed', acc.Description, 'Description should be updated.');
        }
    }
}

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

Batch Apex は、Salesforce プラットフォーム上で大規模なデータセットを扱うための強力で不可欠なツールです。ガバナ制限という制約を乗り越え、何百万ものレコードを効率的かつ確実に処理する能力を提供します。Batch Apex を効果的に活用するためのベストプラクティスを以下にまとめます。

  1. start メソッドのクエリを最適化する:Database.QueryLocator を使用し、WHERE 句でインデックス付き項目を使用して対象レコードを効率的に絞り込みます。不要なデータを取得しないようにしてください。
  2. execute メソッドのロジックを効率化する:各チャンクの処理は、それ自体が1つのトランザクションです。このメソッド内で不必要な SOQL クエリや複雑なループ処理を行うと、CPU 時間制限に達する可能性があります。ロジックはできるだけシンプルかつバルク対応にしてください。
  3. 適切なバッチサイズを選択する:処理の複雑さによってバッチサイズを調整します。デフォルトの200が常に最適とは限りません。テストを通じて、パフォーマンスとリソース消費のバランスが取れたサイズを見つけることが重要です。
  4. Database.Stateful の使用は慎重に:状態の保持が必要な場合にのみ Database.Stateful を使用します。広範囲にわたるデータの保持は、シリアライズのオーバーヘッドによりパフォーマンスを低下させる可能性があります。
  5. 堅牢なエラーハンドリングとログ記録を実装する:大量のデータを処理する際には、予期せぬデータエラーが発生する可能性があります。部分的な成功を許容するDML操作を使用し、失敗したレコードと原因をカスタムオブジェクトなどに記録する仕組みを構築してください。
  6. 包括的なテストカバレッジを確保する:様々なデータパターンやエッジケースを想定したテストクラスを作成し、バッチ処理が期待通りに動作することを保証します。特に、ガバナ制限に達しないかどうかのテストは重要です。

これらの原則とベストプラクティスに従うことで、Salesforce 開発者は Batch Apex を最大限に活用し、スケーラブルで信頼性の高いアプリケーションを構築することができるでしょう。

コメント