Salesforceで10分間隔のバッチスケジュールを設定する方法

# はじめに
こんにちは、murphy97です。
最近Salesforceを使ったプロジェクトに初めて参加することになり、予約内容の確認のために定期的にバッチ処理を実行する必要がありました。一日一回程度の実行であれば、Salesforceの標準機能で簡単に設定できますが、今回のケースでは10分ごとという短い間隔でバッチを実行する必要があり、これが意外と簡単ではありませんでした。

私と同じような要件に直面している方や、Salesforceでの短い間隔のバッチ処理に興味がある方のために、この記事では10分ごとのバッチスケジュール登録方法を詳しく解説します。Salesforce初心者だった私が試行錯誤した経験が、皆さんの業務効率化に役立てば幸いです。

# 背景
プロジェクトでは、予約システムからの情報を定期的に確認し、状態変更があった場合に素早く対応する必要がありました。当初は一日一回の確認を想定していましたが、ビジネス要件の変更により、より頻繁な確認が必要になりました。具体的には：

- 10分ごとに予約データをチェック
- 変更があった場合、関連システムに通知
- 営業時間内に自動運用が必要

このような短い間隔でのバッチ処理は、Salesforceの標準スケジューラーでは直接設定できないため、カスタムの実装方法を検討する必要がありました。
では、この記事を通して、Salesforceで10分ごとのバッチスケジュールを実現する方法を一緒に見ていきましょう。

# 環境設定 & 前提条件
- Salesforce バージョン: v57.0
- 必要な権限: Apexクラスの作成・実行権限
- 必要な知識:
    - Apexの基礎知識（バッチ処理・スケジューラー）
    - 開発者コンソールの使用経験
:::note info
注意: Salesforceではスケジュール登録に設定メニュー、Salesforce CLI、Change Setsなど他の方法も使用できますが、本記事では開発者コンソールでの登録方法を紹介します。チーム開発では通常、Gitなどのバージョン管理システムでSalesforce DXプロジェクト構造を活用して.clsファイルを管理することが一般的です。
:::

これから、10分間隔でのバッチ処理を実現する具体的な実装方法を説明します。

# 実装方法
### 1.バッチ処理用のApexクラス作成
まず、定期的に実行される処理を行うバッチクラスを作成します。このクラスは、`Database.Batchable` インターフェースを実装し、必要に応じて外部APIに接続する場合は `Database.AllowsCallouts` も実装します。

```apex
global class TenMinuteCheckBatch implements Database.Batchable<sObject>, Database.AllowsCallouts {
    
    global Database.QueryLocator start(Database.BatchableContext BC) {
        System.debug('=== Batch Start ===');
    
        // 処理対象のデータを検索
        Datetime now = Datetime.now();
        
        // 検索クエリを実行
        return Database.getQueryLocator([
            SELECT Id, Name, Status__c, LastModifiedDate
            FROM Reservation__c 
            WHERE Status__c = 'Pending'
            AND LastModifiedDate <= :now
        ]);
    }
    
    global void execute(Database.BatchableContext BC, List<SObject> scope) {
        // SObjectをReservation__cに変換
        List<Reservation__c> reservations = new List<Reservation__c>();
        for(SObject obj : scope) {
            reservations.add((Reservation__c)obj);
        }
        
        // バッチ処理のメイン処理
        if(reservations.isEmpty()) {
            System.debug('No reservations found for processing');
            return;
        }
        
        List<Reservation__c> updatedReservations = new List<Reservation__c>();
        
        for(Reservation__c res : reservations) {
            // 予約ステータスの確認と更新
            // 何らかの処理条件に基づいて更新
            if(needsUpdate(res)) {
                res.Status__c = 'Processed';
                updatedReservations.add(res);
            }
        }
        
        // 更新があれば保存
        if(!updatedReservations.isEmpty()) {
            update updatedReservations;
            
            // 外部システムへの通知が必要な場合
            sendNotifications(updatedReservations);
        }
    }
    
    global void finish(Database.BatchableContext BC) {
        System.debug('=== Batch Finish ===');
        // バッチ終了時の処理（ログ記録など）
    }
    
    // 更新が必要かどうかを判定するヘルパーメソッド
    private Boolean needsUpdate(Reservation__c reservation) {
        // 実際の条件に応じてロジックを実装
        return true; // 例として常にtrueを返す
    }
    
    // 通知を送信するヘルパーメソッド
    private void sendNotifications(List<Reservation__c> reservations) {
        // 外部システムへの通知処理を実装
        // HTTP Calloutなどを使用
    }
}
```
:::note info
コード実行のための注意点:
1. 上記コードは、Reservation__cというカスタムオブジェクトとStatus__cフィールドが組織に存在することを前提としています。実際に実行する場合は、組織に存在するオブジェクトに置き換えてください。
2. needsUpdateとsendNotificationsメソッドは実際のビジネスロジックに合わせて実装する必要があります。
3. 外部システムと通信する場合は、Salesforceの「リモートサイト設定」で対象のドメインを許可する必要があります。
4. 実際のプロジェクトでテストする場合は、テストクラスの作成も忘れないようにしましょう。

実行可能な最小限のサンプルとしたい場合は、Reservation__cの代わりに標準オブジェクト（例：Account）を使用することもできます。
:::

### 2. スケジューラ用のApexクラス作成
次に、10分ごとにバッチを実行するスケジューラクラスを作成します。このクラスが本記事の最も重要なポイントです。
```apex
global virtual class TenMinuteCheckScheduler implements Schedulable {
    global void execute(SchedulableContext sc) {
        // 重要: 現在のジョブを終了させる
        if (!Test.isRunningTest()) {
            String jobId = sc.getTriggerId();
            System.abortJob(jobId);
        }

        Datetime now = getNow();
        Integer currentHour = now.hour();
        
        // 営業時間内（9時から18時まで）の場合のみ実行
        if (currentHour >= 9 && currentHour < 18) {
            // バッチを実行
            Database.executeBatch(new TenMinuteCheckBatch());
            
            // 次の10分後のスケジュールを計算
            Integer currentMinute = now.minute();
            Integer nextMinute = (Math.floor(currentMinute/10).intValue() + 1) * 10;
            Datetime nextRun;
            
            if (nextMinute == 60) {
                nextRun = now.addHours(1).addMinutes(-now.minute());
            } else {
                nextRun = now.addMinutes(nextMinute - currentMinute);
            }
            
            // 営業時間外になる場合は翌日の開始時間にスケジュール
            if (nextRun.hour() >= 18) {
                // 翌日の9時に設定
                Datetime tomorrowNine = Datetime.newInstance(
                    now.year(), now.month(), now.day(), 
                    9, 0, 0
                ).addDays(1);
                
                String cronExp = '0 0 9 ' + tomorrowNine.day() + ' ' + 
                                tomorrowNine.month() + ' ? ' + tomorrowNine.year();
                System.schedule('TenMinuteCheck_' + tomorrowNine.getTime(), 
                               cronExp, new TenMinuteCheckScheduler());
            } else {
                // 次の10分間隔で実行
                String cronExp = '0 ' + nextRun.minute() + ' ' + nextRun.hour() + ' ' + 
                                nextRun.day() + ' ' + nextRun.month() + ' ? ' + nextRun.year();
                System.schedule('TenMinuteCheck_' + nextRun.getTime(), 
                               cronExp, new TenMinuteCheckScheduler());
            }
        } else if (currentHour < 9) {
            // 営業時間前なら当日9時に実行されるようにスケジュール
            Datetime todayNine = Datetime.newInstance(
                now.year(), now.month(), now.day(), 
                9, 0, 0
            );
            
            String cronExp = '0 0 9 ' + todayNine.day() + ' ' + todayNine.month() + ' ? ' + todayNine.year();
            System.schedule('TenMinuteCheck_' + todayNine.getTime(), 
                           cronExp, new TenMinuteCheckScheduler());
        }
    }
    
    // テスト用にオーバーライド可能なメソッド
    protected virtual Datetime getNow() {
        return Datetime.now();
    }
}
```
### 実装の重要ポイント
#### 10分間隔を実現するための仕組み
標準のCronスケジュールでは、10分ごとに実行するような柔軟な設定ができません。そこで本実装では、次のような再帰的なアプローチを採用しています：

    1. スケジューラが実行されると、まず現在のジョブを終了（System.abortJob）します
    2. バッチ処理を実行します
    3. 次の10分後の時間を計算し、新しいスケジュールジョブを作成します
    4. これにより、10分ごとに処理が実行される擬似的な定期実行が実現できます

#### 営業時間の考慮
この実装では、営業時間（9時から18時まで）の間だけバッチ処理を実行し、営業時間外になる場合は翌日の営業開始時間（9時）にスケジュールを設定しています。これにより、不要な処理を減らし、システムリソースを効率的に使用できます。

#### 次の実行時間の計算方法
10分間隔で実行するために、現在の分（minute）から次の10分単位の時間を計算する方法がポイントです：
```apex
Integer currentMinute = now.minute();
Integer nextMinute = (Math.floor(currentMinute/10).intValue() + 1) * 10;
```
この計算により、例えば：
- 現在時刻が10:23の場合、次の実行は10:30に設定
- 現在時刻が15:59の場合、次の実行は16:00に設定（60分になる場合は時間を加算）

#### ジョブ名の一意性
スケジュールジョブ名は一意である必要があるため、タイムスタンプを含めています：
```apex
System.schedule('TenMinuteCheck_' + nextRun.getTime(), cronExp, new TenMinuteCheckScheduler());
```
これにより、毎回異なるジョブ名でスケジュールされ、名前の衝突を避けられます。

#### テスト容易性の考慮
getNow()メソッドを仮想化（virtual）しているのは、テストクラスでこのメソッドをオーバーライドすることで、様々な時間帯のテストを容易にするためです。これにより、営業時間内・外の動作を確実にテストできます。

### 3. 開発者コンソールからスケジューラを登録
最後に、開発者コンソールを使って最初のスケジューラを登録します。以下の手順で行います：

1. 開発者コンソールを開く:
    - Salesforce画面右上の歯車アイコン ⚙️ をクリック
    - ドロップダウンメニューから「開発者コンソール」を選択

2. 匿名実行を準備:
    - 開発者コンソールのメニューから「Debug (デバッグ)」→「Open Execute Anonymous Window (実行匿名ウィンドウを開く)」を選択  
    
        ![スクリーンショット 2025-03-03 16.37.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4025715/9dcf5f7a-c0c9-41e9-b07c-6aa7216a4e7c.png)



3. 以下のコードを貼り付けて実行:

    ```apex
    // 開発者コンソールの匿名実行で以下のコードを実行
    Datetime now = Datetime.now();
    Integer currentMinute = now.minute();
    Integer nextMinute = (Math.floor(currentMinute/10).intValue() + 1) * 10;
    Datetime firstRun;
     
    if (nextMinute == 60) {
        firstRun = now.addHours(1).addMinutes(-now.minute());
    } else {
        firstRun = now.addMinutes(nextMinute - currentMinute);
    }
     
    String cronExp = '0 ' + firstRun.minute() + ' ' + firstRun.hour() + ' ' +
                     firstRun.day() + ' ' + firstRun.month() + ' ? ' + firstRun.year();
    System.schedule('TenMinuteCheck_' + firstRun.getTime(), cronExp, new TenMinuteCheckScheduler());
    ```

4. 実行:
    - 「Execute」ボタンをクリックします
    - ログパネルに「Success」と表示されれば登録完了です

このコードは、現在時刻から次の10分単位の時間を計算し、その時間にスケジューラを実行するよう設定します。例えば、現在時刻が15:23の場合、初回実行は15:30に設定されます。

実行後、Salesforceの設定メニューの「設定」→「環境」→「ジョブ」→「スケジュール済みジョブ」で登録されたジョブを確認できます。最初のジョブが実行されると、スケジューラクラスの中で自動的に次の10分後のジョブが登録される仕組みです。

- **最初のスケジュール登録状態** :
「TenMinuteCheck_」という名前で登録されたジョブが表示されています。)
![スクリーンショット 2025-03-03 17.05.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4025715/f4142624-04f2-4b0a-a676-78c4054d2189.png)

- **10分後のスケジュール状態** : 
前回のジョブが実行され終了し、次の10分間隔の新しいジョブが自動的に登録されています。このように、スケジューラクラスが実行されるたびに次の実行のためのジョブを登録する仕組みにより、10分ごとの定期実行が実現されています。
![スクリーンショット 2025-03-03 17.17.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4025715/2677c984-7f7f-4c5f-a11c-36efd78d1dcf.png)


# まとめ
この記事では、Salesforceで10分ごとにバッチ処理を実行する方法について紹介しました。標準のスケジューラー機能では実現できない短い間隔での実行を、再帰的なアプローチで解決しています。
実装のポイントは以下の通りです：

- バッチクラスとスケジューラクラスの組み合わせ
- 次回実行時のスケジュールを自動登録する仕組み
- 営業時間内のみ実行する制御

この方法により、私のプロジェクトでは予約データを短い間隔で確認し、状態変化に迅速に対応できるようになりました。Salesforceの標準機能だけでは実現が難しい要件も、Apexを活用することで柔軟に対応できることを実感しました。

# 参考資料
[Apex スケジューラー](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_scheduler.htm)
[How to schedule apex class after every 5 minutes](https://trailhead.salesforce.com/ja/trailblazer-community/feed/0D54V00007T4UeFSAV)