Apex

Apexバッチをfinish()から再スケジュールする方法

バッチ、スケジュール、コネクタの各クラスを組み合わせ、finish()から次回分をSystem.scheduleへ登録し直す仕組みを説明します。

2023.04.05

なぜ自分で再スケジュールするのか

Setup>Apexクラス>Apexのスケジュールで使えるSchedule Builderは、頻度を時間単位・日単位・週単位・月単位から選ぶ形です。画面から選べる最短の頻度は1時間ごとで、それより短い間隔は選べません。

数分おきのような短い間隔で動かしたいときは、バッチのfinish()メソッドから、次の実行時刻を計算してSystem.schedule()を呼び直す方法を使います。finish()で登録するジョブは、年まで指定したcron式で1回だけ実行されるので、処理が終わるたびに次の1回分を登録し直す仕組みです。

全体の構成

5つのクラスの関係図。スケジューラからバッチ、DAO、ヘルパー、取引先へ矢印がつながる
クラスの関係図です。スケジューラから順に呼ばれ、最後にDAOとヘルパーが取引先を扱う流れを見てください。
クラス役割
BatchSampleScheduleSchedulableを実装し、System.scheduleから呼ばれる入口
BatchSampleConnectIScheduler経由で呼ばれ、実際にバッチを起動するクラス
BatchSampleDatabase.Batchableを実装するバッチ本体。finish()で次回分を登録する
BatchSampleDao取引先レコードを取得するクエリを持つクラス
BatchSampleHelper取得したレコードへの更新をまとめるクラス

BatchSampleScheduleがISchedulerという内部インターフェースを持ち、Type.forName()でクラス名からBatchSampleConnectのインスタンスを動的に作る形になっています。スケジューラ本体を直接書き換えずに、起動するクラスだけ差し替えられる作りです。

コード

BatchSampleSchedule.cls

public with sharing class BatchSampleSchedule implements Schedulable {
    private static final String CONNECT_CLASS = 'BatchSampleConnect';

    public Interface IScheduler {
        void execute(SchedulableContext sc);
    }

    public void execute(SchedulableContext sc) {
        Type targetType = Type.forName(CONNECT_CLASS);
        if (targetType != null) {
            IScheduler connector = (IScheduler) targetType.newInstance();
            connector.execute(sc);
        }
    }
}

System.scheduleから呼ばれる入口です。実際の処理はクラス名を指定してBatchSampleConnectへ委ねます

BatchSampleConnect.cls

public with sharing class BatchSampleConnect implements BatchSampleSchedule.IScheduler {
    public void execute(SchedulableContext sc) {
        Database.executeBatch(new BatchSample(true), 1);
    }
}

スケジューラとバッチ本体をつなぐクラスです。ここでバッチのスコープを指定します

BatchSample.cls

public with sharing class BatchSample implements Database.Batchable<sObject>, Database.Stateful {
    private BatchSampleHelper helper = new BatchSampleHelper();
    private BatchSampleDao dao = new BatchSampleDao();
    private Boolean scheduleNextRun = false;

    public BatchSample() {
    }

    public BatchSample(Boolean scheduleNextRun) {
        this.scheduleNextRun = scheduleNextRun;
    }

    public Database.QueryLocator start(Database.BatchableContext bc) {
        return this.dao.getAccounts();
    }

    public void execute(Database.BatchableContext bc, List<Account> accounts) {
        accounts = this.helper.markAsProcessed(accounts);
        update accounts;
    }

    public void finish(Database.BatchableContext bc) {
        if (this.scheduleNextRun) {
            Datetime nextRun = Datetime.now().addMinutes(1);
            String jobName = 'BatchSampleJob_' + nextRun.format('yyyyMMddHHmmss');
            String cronExp = nextRun.format('0 m H d M ? yyyy');
            System.schedule(jobName, cronExp, new BatchSampleSchedule());
        }
    }
}

取引先を取得して更新するバッチ本体です。finish()で次回1分後の実行を登録します

BatchSampleDao.cls

public with sharing class BatchSampleDao {
    public Database.QueryLocator getAccounts() {
        return Database.getQueryLocator(
            'SELECT Id FROM Account ORDER BY CreatedDate ASC'
        );
    }
}

取引先を作成日の昇順で取得するだけのクエリです

BatchSampleHelper.cls

public with sharing class BatchSampleHelper {
    public List<Account> markAsProcessed(List<Account> accounts) {
        for (Account acc : accounts) {
            acc.IsBatchFlag__c = true;
        }
        return accounts;
    }
}

取得済みのリストへフィールドをセットするだけで、ループ内にDMLはありません

BatchSampleDaoのSOQLは、値を埋め込まない固定のクエリなので、1行の文字列で書いています。値をクエリに埋め込む場合は、文字列連結ではなくバインド変数かString.escapeSingleQuotes()を使ってください。

finish()のcron文字列は、Datetime.format()のパターン文字(分・時・日・月・年)を使って動的に組み立てています。年を含む7項目のcron式になりますが、値は実行のたびに計算した未来の時刻なので、日付を固定で書いているわけではありません。

手動でスケジュールを開始する

取引先の一覧。右端のバッチフラグ列が10行とも空欄のまま
実行前の取引先一覧です。右端の青枠、バッチフラグの列がまだ空欄であることを見てください。
すべてのスケジュール済みジョブの一覧。1件も表示されていない
開始前のスケジュール済みジョブです。1件も登録されていない状態から始めます。
  1. 開発者コンソールを開く

    実行コンソールで匿名Apexを開きます。

  2. 初回のジョブを登録する

    次のコードを実行し、毎日決まった時刻に1回目を起動します。System.schedule('BatchSampleSchedule', '0 43 23 * * ?', new BatchSampleSchedule());

  3. Apexジョブの一覧で確認する

    Setupの「スケジュール済みジョブ」または「Apexジョブ」で、1分後に次のジョブが登録され続けていることを確認します。

取引先の一覧。右端のバッチフラグ列に10行ともチェックが入っている
実行後の取引先一覧です。青枠のバッチフラグにチェックが入り、更新が行き渡ったことを確認します。
スケジュール済みジョブの一覧。次の実行スケジュールの列に1分刻みの時刻が並ぶ
開始後のスケジュール済みジョブです。1分ごとに次の1回分が登録され続けることを見てください。実行者の列は伏せています。

ここで間違えやすい

間違い何が起きるか
finish()で無条件に次回分をスケジュールする処理対象がなくなっても停止条件がないと動き続けます
Database.Statefulを付け忘れるfinish()の時点でscheduleNextRunなどのインスタンス変数が初期値に戻ります
Database.executeBatchのスコープを大きくしすぎるQueryLocatorを使うときのスコープは最大2,000です。それを超える値を指定しても2,000件ずつに分割されます
スケジュール済み・実行中のジョブが増え続ける組織で同時に保持できるスケジュール済み・実行中のApexジョブは100件までです

確認した環境

  • 2026年9月 / Salesforce Summer '26(APIバージョン67.0)時点の公式ドキュメントで、System.schedule・Database.executeBatch・スケジュール済みジョブの上限を確認しています

まとめ

  • Setup画面のSchedule Builderで選べる頻度は1時間ごとが最短です。それより短い間隔で回したいときは、finish()から次回分をコードで登録し直します
  • Database.executeBatchのスコープは、QueryLocatorを使うとき最大2,000です
  • 組織で同時に保持できるスケジュール済み・実行中のApexジョブは100件までです
  • finish()で無条件に次回をスケジュールすると、止め忘れて動き続けます。停止条件を必ず入れてください
  • 値を埋め込まない固定のSOQLは、文字列連結せずに1行で書きます。値を埋め込む場合はバインド変数を使います

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。