Part 9: バッチ処理におけるエラーハンドリング

## **Part 9: バッチ処理 (Batch Apex) におけるエラーハンドリング**  

前回は **Apex トリガーのエラーハンドリング** を解説しました。  
今回は **バッチ処理 (Batch Apex) におけるエラーハンドリング** について解説します。  

---

## **1. バッチ処理におけるエラーハンドリングの考え方**

### **1.1. バッチ処理の特徴**
Apex バッチ (`Database.Batchable`) は、**大量データの処理** に適しています。  
しかし、**一部のレコードでエラーが発生すると、すべてのレコードがロールバックされるわけではない** ため、  
**エラーハンドリングが重要** になります。

バッチ処理の流れ：
1. `start()` メソッドで処理対象のレコードを取得
2. `execute()` メソッドでバッチごとに処理を実行
3. `finish()` メソッドで完了処理（通知やログ記録など）

---

## **2. `execute()` メソッド内のエラーハンドリング**

### **2.1. try-catch でエラーをキャッチする**
**✅ OK 例: try-catch で個々のレコードのエラーを処理**
```apex
global class AccountBatchJob implements Database.Batchable<sObject> {
    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator('SELECT Id, Name FROM Account');
    }

    global void execute(Database.BatchableContext BC, List<Account> scope) {
        List<Account> updateList = new List<Account>();
        for (Account acc : scope) {
            try {
                if (String.isEmpty(acc.Name)) {
                    throw new CustomBatchException('アカウント名が空です: ' + acc.Id);
                }
                acc.Name = acc.Name.toUpperCase();
                updateList.add(acc);
            } catch (CustomBatchException e) {
                ErrorLogService.logError(e, 'AccountBatchJob', 'execute');
            }
        }

        if (!updateList.isEmpty()) {
            update updateList;
        }
    }

    global void finish(Database.BatchableContext BC) {
        System.debug('バッチ処理完了');
    }
}
```
このコードでは：
- `try-catch` で **エラーのあるレコードだけスキップし、正常なレコードを処理**
- `ErrorLogService.logError()` を使って **エラーログを記録**

> **適用ケース:**  
> **一部のエラーは許容しつつ、可能な限り処理を続けたい場合**

---

## **3. `Database.Stateful` を活用してエラーリストを蓄積**

バッチ処理では **各 `execute()` の処理が独立している** ため、  
`Database.Stateful` を使って **エラーリストを蓄積し、最終的にログを出力** する方法もあります。

#### **✅ OK 例: `Database.Stateful` を利用したエラーハンドリング**
```apex
global class AccountBatchJob implements Database.Batchable<sObject>, Database.Stateful {
    private List<String> errorMessages = new List<String>();

    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator('SELECT Id, Name FROM Account');
    }

    global void execute(Database.BatchableContext BC, List<Account> scope) {
        for (Account acc : scope) {
            try {
                if (String.isEmpty(acc.Name)) {
                    throw new CustomBatchException('アカウント名が空です: ' + acc.Id);
                }
                acc.Name = acc.Name.toUpperCase();
            } catch (CustomBatchException e) {
                errorMessages.add(e.getMessage());
            }
        }
    }

    global void finish(Database.BatchableContext BC) {
        if (!errorMessages.isEmpty()) {
            System.debug('エラーリスト: ' + JSON.serialize(errorMessages));
            ErrorLogService.saveErrors(errorMessages); // エラーを保存
        }
    }
}
```
このコードでは：
- `Database.Stateful` を使用して、**エラーメッセージを `finish()` まで保持**
- `finish()` メソッドで **エラーログを一括保存**

> **適用ケース:**  
> **エラー情報をまとめてログ出力したい場合**

---

## **4. `Database.Savepoint` でトランザクションを制御する**
バッチ処理の **トランザクション単位でロールバックする** 方法として、`Database.Savepoint` を活用できます。

#### **✅ OK 例: `Savepoint` を活用したリカバリ**
```apex
global class AccountBatchJob implements Database.Batchable<sObject> {
    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator('SELECT Id, Name FROM Account');
    }

    global void execute(Database.BatchableContext BC, List<Account> scope) {
        Database.Savepoint sp = Database.setSavepoint();
        try {
            update scope; // 一括更新
        } catch (DmlException e) {
            Database.rollback(sp); // エラー時にロールバック
            for (Account acc : scope) {
                try {
                    update acc; // 1 レコードずつ更新
                } catch (DmlException ex) {
                    ErrorLogService.logError(ex, 'AccountBatchJob', 'execute');
                }
            }
        }
    }

    global void finish(Database.BatchableContext BC) {
        System.debug('バッチ処理完了');
    }
}
```
このコードでは：
- `Database.Savepoint` で **スコープ単位の更新を試行**
- **失敗した場合はロールバックし、1 件ずつリトライ**

> **適用ケース:**  
> **一括更新時にエラーが発生しても、可能な限り処理を続けたい場合**

---

## **5. エラー通知を `finish()` で送信**
バッチ処理でエラーが発生した場合、**管理者にメール通知** するのも有効です。

#### **✅ OK 例: `Messaging.SingleEmailMessage` でエラーメール通知**
```apex
global void finish(Database.BatchableContext BC) {
    if (!errorMessages.isEmpty()) {
        String body = 'バッチ処理でエラーが発生しました。\n' + String.join(errorMessages, '\n');
        
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        mail.setToAddresses(new String[] {'admin@example.com'});
        mail.setSubject('【警告】バッチ処理のエラー');
        mail.setPlainTextBody(body);
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] {mail});
    }
}
```
このコードでは：
- `Messaging.SingleEmailMessage` を使って **エラーリストをメールで通知**

> **適用ケース:**  
> **エラー発生時に管理者へ即時通知したい場合**

---

## **まとめ**
今回は **Apex バッチ処理のエラーハンドリング** を解説しました。

- **`try-catch` で個々のレコードのエラーを処理**
- **`Database.Stateful` でエラーリストを蓄積し、`finish()` で一括保存**
- **`Savepoint` を活用して一括更新のリカバリを行う**
- **エラー通知を `Messaging.SingleEmailMessage` で送信**

> **次の Part 10** では、**フューチャーメソッド (Future Methods) や Queueable Apex におけるエラーハンドリング** を解説します！