2023.09.11

Apex一括処理(バッチ処理)のエラーハンドリング方法紹介

はじめに

Apexにて大量データを扱う場合、Apex一括処理(以下、バッチ処理)を使用することが多いのではないでしょうか。
さらに、弊社で販売しているGLOVIA OMなどを使用する場合は扱うデータの特性上、トランザクション数が増加することが多いため、バッチ処理を使用する機会が増えるかと思います。

大量データ処理イメージ

バッチ処理は大量データを扱う性質上、整合性を保つための考慮などで制御が難しくなり、エラーが発生することも多く、さらにはロールバックなどが難しい処理になります。
そのため、エラーが何に対して起きたのかなどを把握できるようにバッチ処理のエラーハンドリングを正しく行うことが重要です。
しかしながら、コントローラーなどの実装のようにtry~catchからのエラーハンドリングだけでは検知できません。

今回はバッチ処理を実装する際のエラーハンドリングについて紹介していきます。

エラーハンドリングができていない場合にどうなるのか

そもそもエラーハンドリング出来ていない場合にどうのようになるのかを紹介します。

例1.try~catch可能なエラー
 try~catch可能なエラーはcatch句の実装でエラーハンドリングが可能です。

 実装例)
 execute実行時、計算処理にnullが混ざっていたため、NullPointerExceptionが発生。
 execute処理内をtry~catchしているため、 catch句にてエラーハンドリングが可能。
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        try {
            Integer sumVal = 0;
            for (Account acc : scope) {
                sumVal += acc.NumberOfEmployees;
            }
        } catch (Exception ex) {
            // catch可能なため、エラーハンドリングが可能
            // カスタムオブジェクトのErrorLogへエラー内容を保存する
            ErrorLog__c errLog = new ErrorLog__c();
            errLog.ErrMessage__c = ex.getMessage();
            errLog.StackTrace__c = ex.getStackTraceString();
            insert errLog;
        }
    }
NullPointerExceptionが発生するバッチクラス(executeのみ抜粋)
例2.ガバナエラー
 ガバナエラーはtry~catch出来ないため、catch句によるエラーハンドリングが出来ません。

 実装例)
 execute実行時、DML実行回数が閾値の200回を超えたため、ガバナエラーが発生。
 execute処理内をtry~catchしているが、 catchされず、エラーハンドリングが行えない。
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        try {
            // ガバナエラー
            for (Integer i=0;i<201;i++) {
                Account test = [select id from Account where id = :scope[0].id];
            }
        } catch (Exception ex) {
            // catchされないため、エラーハンドリングが出来ない
            // カスタムオブジェクトのErrorLogへエラー内容を保存する
            ErrorLog__c errLog = new ErrorLog__c();
            errLog.ErrMessage__c = ex.getMessage();
            errLog.StackTrace__c = ex.getStackTraceString();
            insert errLog;
        }
    }
System.Exception: Too many SOQL queriesが発生するバッチクラス(executeのみ抜粋)

エラーハンドリング方法の紹介

先の問題はSalesforceから提供されている、Database.RaisesPlatformEventsBatchApexErrorEventを使用することでハンドリング可能になります。

それぞれの使用方法を紹介します。

①Database.RaisesPlatformEventsの適用

バッチクラスにDatabase.RaisesPlatformEventsをimplementします。
public with sharing class TestBatch implements Database.Batchable<sObject>, Database.RaisesPlatformEvents {
TestBatch.cls(クラス定義部分のみ抜粋)
行うことは以上です。
これを実装することによって、バッチクラスからエラーがThrowされた場合にプラットフォームイベントにてBatchApexErrorEventが登録されます。

ただし、BatchApexErrorEventはプラットフォームイベントのため、レコードには保存されません。
このままでは何も残らないので「プラットフォームイベント受信側の実装」が必要となります。

②BatchApexErrorEvent受信処理を実装

①で登録されたプラットフォームイベントをハンドリングするため、BatchApexErrorEventトリガーを実装します。
trigger BatchApexErrorEventTrg on BatchApexErrorEvent (after insert) {
    // エラーハンドリング処理を実装(記載は省略)
}
BatchApexErrorEventのトリガークラス
①で設定したDatabase.RaisesPlatformEventsにて、Salesforceがエラー内容をBatchApexErrorEventプラットフォームイベントで登録してくれます。
BatchApexErrorEventをトリガークラスにて、任意のエラーハンドリング処理を実装することで、
NullPointerExceptionや入力規則エラーなどのthrowされたException、DML発行数超過などのガバナエラーの内容をハンドリング可能になります。

※補足
今回の例ではトリガーを使用したエラーハンドリングを実装していますが、プラットフォームイベントの受信はフローを使用することも可能です。

実装例とエラーハンドリング結果の紹介

改めて、実装例とエラー処理結果を紹介します。

この実装例では、エラーの内容をカスタムオブジェクトである「エラーログオブジェクト」へ保存するというエラーハンドリングを行います。

TestBatch.cls

TestBatchを実行することで、try~catchでは捕捉できないガバナエラーが発生します。
Database.RaisesPlatformEventsを使用してエラーハンドリングを行うため、try~catchの記載は不要です。
public with sharing class TestBatch implements Database.Batchable<sObject>, Database.RaisesPlatformEvents {

    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator([SELECT id,name,NumberOfEmployees FROM Account]);
    }

    // エラー発生のテストのため、処理に意味はありません
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        for (Integer i=0;i<201;i++) {
            Account acc = [select id from Account where id = :scope[0].id];
        }
    }

    public void finish(Database.BatchableContext bc) {
        system.debug('TestBatch finish');
    }
}
TestBatch.cls

BatchApexErrorEventTrg.trigger

BatchApexErrorEventTrgでプラットフォームイベントを受信します。
実際の処理はBatchApexErrorEventHandler.clsにて行います。
trigger BatchApexErrorEventTrg on BatchApexErrorEvent (after insert) {
    // 実処理はトリガーハンドラーにて実行する
    BatchApexErrorEventHandler triggerHandler = new BatchApexErrorEventHandler();
    triggerHandler.onAfterInsert(Trigger.new);
}
BatchApexErrorEventTrg.trigger

BatchApexErrorEventHandler.cls

受信したBatchApexErrorEventの内容をカスタムオブジェクトの「エラーログ」へInsertします。
BatchApexErrorEventそのままではエラーが発生したクラス名が不明なため、AsyncApexJobとApexClassオブジェクトを検索し、クラス名を取得するように工夫しています。
public with sharing class BatchApexErrorEventHandler {
    private Map<Id, AsyncApexJob> jobMap;
    private Map<Id, ApexClass> classMap;

    public void onAfterInsert(List<BatchApexErrorEvent> newList){
        // Apexクラス名を取得
        getClassInfo(newList);
        // エラーハンドリング
        errorHandler(newList);
    }

    private void getClassInfo(List<BatchApexErrorEvent> errorEventList){
        Set<Id> asyncApexJobIds = new Set<Id>();
        for(BatchApexErrorEvent evt : errorEventList){
            asyncApexJobIds.add(evt.AsyncApexJobId);
        }
        jobMap = new Map<Id, AsyncApexJob>([SELECT Id, ApexClassID FROM AsyncApexJob WHERE Id IN :asyncApexJobIds]);
        Set<Id> classIds = new Set<Id>();
        for(AsyncApexJob job : jobMap.values()){
            classIds.add(job.ApexClassID);
        }
        classMap = new Map<Id, ApexClass>([SELECT Id, Name FROM ApexClass WHERE Id IN :classIds]);
    }

    private void errorHandler(List<BatchApexErrorEvent> errorEventList) {
        // 任意のエラーハンドリングを行う
        // insert用バッチエラーログList
        List<ErrorLog__c> errLogList = new List<ErrorLog__c>();
        for (BatchApexErrorEvent errorEvent: errorEventList) {
            ErrorLog__c errLog = new ErrorLog__c();
            // ApexジョブID
            errLog.AsyncApexJobId__c = errorEvent.AsyncApexJobId;
            // Apexクラス名
            errLog.ApexClassName__c = classMap.get(jobMap.get(errorEvent.AsyncApexJobId).ApexClassID).Name;
            // 例外種別名
            errLog.ExceptionType__c = errorEvent.ExceptionType;
            // スコープ
            errLog.JobScope__c = errorEvent.JobScope;
            // エラーメッセージ
            errLog.ErrMessage__c = errorEvent.Message;
            // 発生フェーズ
            errLog.Phase__c = errorEvent.Phase;
            // リプレイID
            errLog.ReplayId__c = errorEvent.ReplayId;
            // スタックトレース
            errLog.StackTrace__c = errorEvent.StackTrace;
            // insert用Listに格納
            errLogList.add(errLog);
        }
        // insert
        if (errLogList.size() > 0) {
            insert errLogList;
        }
    }
}
BatchApexErrorEventHandler.cls

登録したエラーログの確認

カスタムオブジェクトのエラーログを確認することで、ガバナエラーの内容を確認することができるようになります。

この内容をシステム課宛へメール自動送信させるなど、運用に沿った柔軟な対応が可能になります。

受信結果情報

おわりに

以上がApex一括処理(バッチ処理)のエラーハンドリング方法になります。

最初にも記載しましたが、論理エラーやロジックミスのみを想定したtry~catchでは正しくエラーハンドリングは行えません。
ガバナエラーはシステムの運用開始後にすぐには発生せず、運用開始の半年後に発生することもあり得ます。

エラーハンドリングが実装されていない場合は、エラー通知はSalesforceからシステムメールがSalesforce管理者へ送信されるのみになります。
これにすぐ気付けず、数週間後にエラーの内容と影響範囲などを確認するのはかなりの負担になります。

バッチ処理を実装する際にはDatabase.RaisesPlatformEvents、BatchApexErrorEventの適用をぜひ検討してみてください。
36 件
     
  • banner
  • banner

関連する記事