Salesforce バッチ Apex のマスター:大規模データセットを処理するための開発者ガイド

Salesforce 開発者として、私たちが日常的に直面する課題の一つに、大量のデータを効率的かつ安全に処理することがあります。Salesforce はマルチテナント環境であるため、すべてのユーザーに公平なパフォーマンスを保証するために、トランザクションごとにリソースの使用量を制限する Governor Limits(ガバナ制限)が存在します。例えば、1つのトランザクション内で発行できる SOQL (Salesforce Object Query Language) クエリは100回まで、処理できる DML (Data Manipulation Language) レコードは10,000件までと定められています。数万、数百万のレコードを一度に更新しようとすると、これらの制限に容易に抵触してしまいます。

この課題を解決するために Salesforce が提供している強力なツールが、Asynchronous Apex(非同期 Apex)の一つである Batch Apex(バッチ Apex)です。本記事では、Salesforce 開発者の視点から Batch Apex の仕組みを深く掘り下げ、その効果的な活用方法をサンプルコードと共に解説します。


背景と適用シナリオ

Batch Apex は、大量のレコードを小さな「バッチ」または「チャンク」に分割し、それぞれを独立したトランザクションとして処理するフレームワークです。これにより、Governor Limits を回避しながら、最大5,000万件ものレコードを処理することが可能になります。

具体的な適用シナリオ

  • データクレンジング: 組織内の全取引先レコードの電話番号形式を統一したり、住所データを標準化したりするなど、大規模なデータ整理作業。
  • データ移行・アーカイブ: 古い活動履歴やケースレコードを抽出し、外部システムに連携したり、カスタムのアーカイブオブジェクトに移動したりする定期的なバッチ処理。
  • 複雑なビジネスロジックの実行: 全ての商談品目に基づいて、取引先ごとに年間の総売上を再計算し、カスタム項目を更新するような、CPU 処理時間を要する複雑な計算。
  • 外部システムとのデータ同期: 毎日深夜に、外部の ERP システムから取得した最新の製品在庫情報を、Salesforce の商品オブジェクトに一括で反映させる処理。

これらのシナリオでは、同期処理では Governor Limits に抵触する可能性が非常に高く、Batch Apex の利用が不可欠となります。


原理説明

Batch Apex を実装するには、グローバルまたはパブリックな Apex クラスで Salesforce が提供する Database.Batchable インターフェースを実装する必要があります。このインターフェースには、3つの必須メソッドが含まれています。

1. start() メソッド

このメソッドは、バッチジョブの開始時に一度だけ呼び出されます。その役割は、処理対象となるすべてのレコードを特定し、その集合を返すことです。戻り値の型は、Database.QueryLocator または Iterable のいずれかです。

  • Database.QueryLocator: シンプルな SOQL クエリで対象レコードを取得する場合に使用します。最大5,000万件のレコードを効率的に処理できるため、大量の標準オブジェクトやカスタムオブジェクトを処理する際の第一選択肢となります。SOQL クエリのガバナ制限(取得50,000件)が適用されません。
  • Iterable: 外部ウェブサービスから取得したデータや、複雑なロジックで構築されたリストなど、SOQL だけで取得できないデータセットを処理する場合に使用します。

2. execute() メソッド

このメソッドが Batch Apex の心臓部です。start メソッドが返したレコードセットは、指定されたバッチサイズ(デフォルトは200)のチャンクに分割され、そのチャンクごとに execute メソッドが呼び出されます。例えば、10,000件のレコードをバッチサイズ200で処理する場合、execute メソッドは50回呼び出されます。

重要な点として、execute メソッドの各呼び出しは、それぞれが独立した Apex トランザクションとして扱われます。 これは、各チャンクの処理が独自の Governor Limits を持つことを意味します。これにより、全体の処理が単一トランザクションの制限に縛られることなく、スケールすることが可能になります。

このメソッドは、Database.BatchableContext オブジェクトと、処理対象レコードのリスト(例: List)を引数として受け取ります。

3. finish() メソッド

すべてのバッチ(チャンク)の処理が完了した後に、一度だけ呼び出されるメソッドです。後処理アクションを実行するのに最適な場所です。

例えば、処理結果をまとめたサマリーメールを管理者に送信したり、エラーログを集計してカスタムオブジェクトに保存したり、このバッチジョブの結果を受けて次のバッチジョブを起動(ジョブの連鎖)したりする処理をここで行います。

状態の維持: Database.Stateful

デフォルトでは、execute メソッドの各トランザクションはステートレスです。つまり、あるチャンクの処理でクラスのメンバー変数に値を設定しても、次のチャンクの処理が始まる際にはその値はリセットされています。もし、バッチジョブ全体を通して、処理したレコードの総数や発生したエラーの総数などをカウントしたい場合は、クラス定義時に Database.Stateful インターフェースを実装します。これにより、メンバー変数の値がトランザクションをまたいで維持されるようになります。ただし、オブジェクトの状態をシリアライズ・デシリアライズするオーバーヘッドが発生するため、必要な場合にのみ使用することが推奨されます。


サンプルコード

以下に、Salesforce 公式ドキュメントで紹介されている典型的な Batch Apex の例を示します。このコードは、組織内のすべての取引先(Account)レコードを検索し、その説明(Description)項目を更新するものです。

global class UpdateAccountFields implements Database.Batchable<sObject> {

    // startメソッド:処理対象のレコードをSOQLクエリで取得し、QueryLocatorとして返す
    global Database.QueryLocator start(Database.BatchableContext bc) {
        // 更新対象の取引先レコードを特定するSOQLクエリ
        // ここではすべての取引先を対象にしている
        return Database.getQueryLocator(
            'SELECT Id, Name, Description FROM Account'
        );
    }

    // executeメソッド:分割されたレコードのチャンク(リスト)を受け取り、実際の処理を行う
    global void execute(Database.BatchableContext bc, List<Account> scope) {
        // 'scope' には、このトランザクションで処理する取引先のリストが含まれる
        // (デフォルトでは最大200件)
        List<Account> accountsToUpdate = new List<Account>();
        for (Account acc : scope) {
            // ここに各レコードに対するビジネスロジックを記述
            // この例では、説明項目に更新情報を追記している
            acc.Description = 'Batch Apexによって更新されました。';
            accountsToUpdate.add(acc);
        }
        
        // 変更したレコードを一括で更新する
        // DML操作はループの外で行うのがベストプラクティス
        if (!accountsToUpdate.isEmpty()) {
            update accountsToUpdate;
        }
    }

    // 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('取引先更新バッチ処理完了: ' + job.Status);
        mail.setPlainTextBody(
            'バッチジョブが完了しました。\n' + 
            '処理済みアイテム数: ' + job.JobItemsProcessed + '\n' +
            'エラー数: ' + job.NumberOfErrors
        );
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
}

Batch Apex の実行方法

作成した Batch Apex は、開発者コンソールの匿名実行ウィンドウなどから以下のように実行します。

// バッチクラスのインスタンスを作成
UpdateAccountFields updateJob = new UpdateAccountFields();
// executeBatchメソッドでジョブを開始
// 第2引数(scope)は、executeメソッドに渡されるレコードの最大数を指定する(オプション)
// 指定しない場合のデフォルトは200
ID jobId = Database.executeBatch(updateJob, 100);

Database.executeBatch メソッドは、ジョブの ID を返します。この ID を使って、[設定] > [ジョブ] > [Apex ジョブ] で処理の進行状況を監視できます。


注意事項

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

権限とセキュリティ

Batch Apex は、ジョブを開始したユーザーの権限で実行されます。そのため、そのユーザーが処理対象のオブジェクトや項目に対する適切な参照・更新権限を持っていることを確認する必要があります。

Governor Limits(ガバナ制限)

前述の通り、execute メソッドの各呼び出しは独立したトランザクションとして扱われ、それぞれにガバナ制限が適用されます。しかし、組織全体で共有される非同期処理の制限も存在します。例えば、24時間以内に実行できる非同期 Apex(Batch Apex, Queueable Apex, Future メソッドなど)の総数には上限があります。大規模なデータを頻繁に処理するバッチを設計する際は、この全体的な制限も考慮に入れる必要があります。

エラー処理

execute メソッド内でエラーが発生した場合、デフォルトではそのチャンク全体のトランザクションがロールバックされます。しかし、チャンク内の一部のレコードが原因で、他の正常なレコードの処理まで失敗するのは非効率です。Database.update(records, false) のように、DML 操作の第2引数に false を指定することで、部分的な成功(Partial Success)を許可できます。これにより、エラーが発生したレコードを除いて、正常なレコードの更新はコミットされます。DML の戻り値である Database.SaveResult を確認し、失敗したレコードの ID とエラーメッセージを収集・記録し、finish メソッドで通知する、といった堅牢なエラーハンドリングを実装することが重要です。

同時実行ジョブの制限

一度に「処理中」または「待機中」の状態にできる Batch Apex ジョブは5つまでという制限があります。6つ目のジョブを投入しようとするとエラーが発生します。バッチジョブを連鎖させる設計や、複数のバッチを同時にスケジュールする際には、この制限に注意が必要です。

コールアウト (Callouts)

Batch Apex から外部システムの API を呼び出す(コールアウトする)場合は、クラス定義で Database.AllowsCallouts インターフェースを追加で実装する必要があります。

テスト

Batch Apex のテストは必須です。テストクラスでは、Test.startTest()Test.stopTest() のブロック内で Database.executeBatch を呼び出します。Test.stopTest() が実行されると、システムは非同期であるバッチジョブを同期的に実行し、完了させます。これにより、stopTest の後で、バッチ処理後のデータ状態をアサーション(System.assertEquals など)で検証することができます。


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

Batch Apex は、Salesforce Platform 上で大量のデータを扱う開発者にとって不可欠なツールです。Governor Limits という制約を乗り越え、スケーラブルで信頼性の高いデータ処理を実現します。

ベストプラクティス

  1. start メソッドのクエリは具体的に: SELECT * FROM ... のようなクエリは避け、処理に必要な項目のみを明示的に指定してください。これにより、ヒープサイズの使用量を最小限に抑えられます。
  2. バッチサイズの調整: execute メソッド内のロジックが非常に複雑で CPU 時間を多く消費する場合や、コールアウトを行う場合は、Database.executeBatch の第2引数でバッチサイズをデフォルトの200より小さく設定することを検討してください。これにより、タイムアウトエラーを回避できます。
  3. Database.Stateful の慎重な利用: 状態の維持は便利ですが、パフォーマンスに影響を与える可能性があります。本当に必要な場合にのみ使用し、保持するデータは最小限に留めましょう。
  4. 冪等性(Idempotence)の確保: バッチジョブは、何らかの理由で再実行される可能性があります。設計の際には、同じジョブが複数回実行されても、データが意図しない状態にならないように(冪等性を保つように)考慮することが重要です。
  5. AsyncApexJob オブジェクトの監視: バッチジョブの実行状況は AsyncApexJob オブジェクトを SOQL でクエリすることでプログラムから追跡できます。finish メソッドでこのオブジェクトを参照し、処理結果を正確に把握しましょう。

これらの原理とベストプラクティスを理解し、適切に Batch Apex を活用することで、Salesforce 開発者として、より堅牢でスケーラブルなソリューションを構築することができるでしょう。

コメント