Spring Batch初期構築 ~JobRepositoryの保存先設定から実行まで~

# はじめに
業務でバッチアプリケーションの開発を行うことになったため、まずは動くバッチアプリを作るぞ！ということで、
こちらのQiitaの記事を参考に進めていました。**（以降、この記事内の工程をやった前提で進みます）**
[まずは実践、Spring Boot Batchの動かし方](https://qiita.com/kawakawaryuryu/items/4b3f5cc7574b7bd6b625)

しかし開発を進めたところ、[JobRepositoryの保存先の設定や起動の箇所](https://qiita.com/kawakawaryuryu/items/4b3f5cc7574b7bd6b625#datasource%E3%81%AE%E8%A8%AD%E5%AE%9A)で躓いてしまい、かなりてこずったので、
本記事では**JobRepositoryの保存先の設定と実行手順について説明**しようと思います。

※上記の記事は、基本的なSpring Batchについての概念も丁寧に説明されていますので、
公式リファレンスと一緒に参考されるとよいかと思います。


# 前提
今回紹介する内容は、基本的にこちらで解説されている内容 +α の部分になります。
[まずは実践、Spring Boot Batchの動かし方](https://qiita.com/kawakawaryuryu/items/4b3f5cc7574b7bd6b625)
記事を読まれる方は実際にこちらを試し、同じ場所で詰まった...という時に参考いただければと思います。

また、私の場合はJobRepositoryの保存先としてh2ではなくMySQLにしていますので、こちらについてもご了承ください。
datasourceのymlのMySQLバージョンは後述。

## バージョン
|使用技術  | バージョン |
|:-----|:--------|
|Spring Boot |2.7.2 |
|spring-boot-starter-batch |2.7.2 |
|Java  |11     |
|MySQL |8.0.30 |

見本にした記事と違うところとしては、JobRepositoryの保存先としてh2ではなくMySQLにしたことです。


### 依存関係
```xml:pom.xml
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>
```


### プロパティの設定
```yml:application.yml
spring:
  datasource:
    url: "jdbc:mysql://localhost/[DB名]"
    username: 
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver
```
※username, passwordは構築した時に設定した値


MySQLなどの環境構築は記事がたくさんあるので、そちらを参考ください。

# 本編
ここから本編に入ります。

## 一度実行してみる
TaskletやJob, Stepの実装を終え、プロパティでJobRepositoryの保存先になるDBの情報を定義したところで一度実行してみます。
実行方法ですが、私はEclipseで行っていたので、`Spring Boot アプリケーションの実行`にて行いました。

```console
java.lang.IllegalStateException: Failed to execute ApplicationRunner

～省略～

Caused by: org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [SELECT JOB_INSTANCE_ID, JOB_NAME from BATCH_JOB_INSTANCE where JOB_NAME = ? and JOB_KEY = ?]; nested exception is java.sql.SQLSyntaxErrorException: Table 'upload_job_execution.batch_job_instance' doesn't exist
```
スタックトレースでは **「SQLを発行しようとしたけどテーブルが存在しませんよ。」** と出力されています。
当たり前ですね。まだテーブル作成のDDLを流していないからです。

ひとまずこの時点で、実行するとジョブやステップの処理が進み、JobRepositoryがプロパティで定義した保存先に格納しようと





## ちなみにJobRepositoryとは？
簡単に言うと、バッチアプリケーション実行結果の情報を永続化させる機能を提供する仕組みのことです。

:::note info
JobExecutionやStepExecutionなどのバッチアプリケーション実行結果や状態を管理するためのデータを管理、永続化する機能を提供する。

<details><summary>続き...</summary>

一般的なバッチアプリケーションはJavaプロセスを起動することで処理が開始し、処理の終了ともにJavaプロセスも終了させるケースが多い。 そのためこれらのデータはJavaプロセスを跨いで参照される可能性があることから、揮発性なメモリ上だけではなくデータベースなどの永続層へ格納する。 データベースに格納する場合は、JobExecutionやStepExecutionを格納するためのテーブルやシーケンスなどのデータベースオブジェクトが必要になる。
Spring Batch が提供するスキーマ情報を元にデータベースオブジェクトを生成する必要がある。
</details>
:::

[引用：Spring Batchのアーキテクチャ terasoluna-batch.github.io](https://terasoluna-batch.github.io/guideline/5.0.0.RELEASE/ja/Ch02_SpringBatchArchitecture.html)



## JobRepositoryの保存先設定
スタックトレースで出力されていた通り、テーブルを用意します。
DDLはこちらで公開されているものを使ってみようとしました。

~~[Spring Batch JobRepository用 DDL](https://github.com/spring-projects/spring-batch/blob/main/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mysql.sql)~~

が、古いバージョン用のため、カラムのDEFAULT値などが違うと怒られてしまいます。
なので、下記を参考にDDLを修正しました。

[stackoverflow MySQL用のSpring Batch 3 アップグレードスクリプト](https://stackoverflow.com/questions/25044725/is-there-a-spring-batch-3-upgrade-script-for-mysql)


<details><summary>JobRepository DDL</summary>

```sql
CREATE TABLE BATCH_JOB_INSTANCE  (
	JOB_INSTANCE_ID BIGINT  NOT NULL PRIMARY KEY ,
	VERSION BIGINT ,
	JOB_NAME VARCHAR(100) NOT NULL,
	JOB_KEY VARCHAR(32) NOT NULL,
	constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
) ENGINE=InnoDB;

CREATE TABLE BATCH_JOB_EXECUTION  (
	JOB_EXECUTION_ID BIGINT  NOT NULL PRIMARY KEY ,
	VERSION BIGINT  ,
	JOB_INSTANCE_ID BIGINT NOT NULL,
	CREATE_TIME DATETIME(6) NOT NULL,
	START_TIME DATETIME(6) DEFAULT NULL ,
	END_TIME DATETIME(6) DEFAULT NULL ,
	STATUS VARCHAR(10) ,
	EXIT_CODE VARCHAR(2500) DEFAULT NULL,
	EXIT_MESSAGE VARCHAR(2500) ,
	LAST_UPDATED DATETIME(6),
	JOB_CONFIGURATION_LOCATION varchar(2500) DEFAULT NULL,
	constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
	references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)
) ENGINE=InnoDB;

CREATE TABLE BATCH_JOB_EXECUTION_PARAMS  (
	JOB_EXECUTION_ID BIGINT NOT NULL ,
	TYPE_CD VARCHAR(6) NOT NULL ,
	KEY_NAME VARCHAR(100) NOT NULL ,
	STRING_VAL VARCHAR(250) ,
	DATE_VAL DATETIME(6) DEFAULT NULL ,
	LONG_VAL BIGINT ,
	DOUBLE_VAL DOUBLE PRECISION ,
	IDENTIFYING CHAR(1) NOT NULL ,
	constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)
	references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;

CREATE TABLE BATCH_STEP_EXECUTION  (
	STEP_EXECUTION_ID BIGINT  NOT NULL PRIMARY KEY ,
	VERSION BIGINT NOT NULL,
	STEP_NAME VARCHAR(100) NOT NULL,
	JOB_EXECUTION_ID BIGINT NOT NULL,
	CREATE_TIME DATETIME(6) DEFAULT NULL,
	START_TIME DATETIME(6) DEFAULT NULL ,
	END_TIME DATETIME(6) DEFAULT NULL ,
	STATUS VARCHAR(10) ,
	COMMIT_COUNT BIGINT ,
	READ_COUNT BIGINT ,
	FILTER_COUNT BIGINT ,
	WRITE_COUNT BIGINT ,
	READ_SKIP_COUNT BIGINT ,
	WRITE_SKIP_COUNT BIGINT ,
	PROCESS_SKIP_COUNT BIGINT ,
	ROLLBACK_COUNT BIGINT ,
	EXIT_CODE VARCHAR(2500) DEFAULT NULL,
	EXIT_MESSAGE VARCHAR(2500) ,
	LAST_UPDATED DATETIME(6),
	constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID)
	references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;

CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT  (
	STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
	SHORT_CONTEXT VARCHAR(2500) NOT NULL,
	SERIALIZED_CONTEXT TEXT ,
	constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)
	references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)
) ENGINE=InnoDB;

CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT  (
	JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
	SHORT_CONTEXT VARCHAR(2500) NOT NULL,
	SERIALIZED_CONTEXT TEXT ,
	constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)
	references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;

CREATE TABLE BATCH_STEP_EXECUTION_SEQ (
	ID BIGINT NOT NULL,
	UNIQUE_KEY CHAR(1) NOT NULL,
	constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;

INSERT INTO BATCH_STEP_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_STEP_EXECUTION_SEQ);

CREATE TABLE BATCH_JOB_EXECUTION_SEQ (
	ID BIGINT NOT NULL,
	UNIQUE_KEY CHAR(1) NOT NULL,
	constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;

INSERT INTO BATCH_JOB_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_EXECUTION_SEQ);

CREATE TABLE BATCH_JOB_SEQ (
	ID BIGINT NOT NULL,
	UNIQUE_KEY CHAR(1) NOT NULL,
	constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;

INSERT INTO BATCH_JOB_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_SEQ);

```
</details>

## バッチ処理実行
JobRepositoryの保存先となるテーブルの準備ができたので、実行すると成功しました。


### この段階でエラーになる場合
Spring Batchのバージョン違いの理由でDDLの内容に不備がある場合があり得ますので、
スタックトレースの内容を読んでSQLException関連の内容であれば、対応することで実行できると思います。


## ジョブの再実行
一度ジョブを実行し、成功するとテーブルにジョブの実行情報が格納されます。[1レコード目]
このまま開発を進めていく中で再度実行しようとすると、2レコード目のように
**「すべてのステップがすでに完了しています」** とメッセージが格納されていることがわかります。
また、実行時には例外をスローします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/579117/56fb7199-f82b-b197-db52-0daa027d04bf.png)


これを解決する方法としては、実行の度にテーブルを初期化することです。
開発段階では一旦このように、消してしまっても問題ありません。
ということで、実行の度にテーブルを初期化するよう設定します。

### 実行時のテーブル初期化設定

まず初期化用のSQLを`/src/main/resources`配下に配置します。ファイル名はなんでも良いです。
今回は`job_execution_init.sql`にしました。
```SQL:job_execution_init.sql
DELETE FROM BATCH_STEP_EXECUTION_CONTEXT;
DELETE FROM BATCH_JOB_EXECUTION_CONTEXT;
DELETE FROM BATCH_JOB_EXECUTION_PARAMS;
DELETE FROM BATCH_STEP_EXECUTION;
DELETE FROM BATCH_JOB_EXECUTION;
DELETE FROM BATCH_JOB_INSTANCE;
```
他の記事によく記載されていた、Drop TABLEやtruncateは外部キー制約があるので消せない！
とエラーが出ましたので、DELETE文にしました。

次に、プロパティファイルに
- `mode: always`
`always`とすることで、常にテーブル初期化処理を行います。
- `schema-locations`
初期化用のSQLの配置先を定義します。`/src/main/resources`配下であればこの書き方で一旦良いです。

```yml:application.yml
  # 実行の度にDB初期化
  sql:
    init:
      mode: always
      schema-locations:
      - classpath:job_execution_init.sql
```

**・補足**
`spring.datasource.initialization-mode`は現在、非推奨（廃止予定）になっています。

これで再度実行すると、
**処理前にテーブル初期化 → バッチ処理 → JobRepositoryが実行情報を保存**
の順に処理され、実行を繰り返し行えるようになります。


# まとめ
JobRepositoryの保存先になるDBにはテーブルを作っておく。
DDLは最新でない可能性があることを認識しておく。
再度実行のために、テーブル内データの初期化処理を作成し、プロパティで設定しておく。


# 参考
[Spring Batch 公式リファレンス](https://spring.io/projects/spring-batch)
[Spring Boot リファレンスドキュメント](https://spring.pleiades.io/spring-boot/docs/current/reference/html/howto.html#howto.batch)
[JobRepository の設定](https://spring.pleiades.io/spring-batch/docs/4.3.6/reference/html/job.html#configuringJobRepository)
[ジョブの状態管理 Entityやテーブル名の情報](https://terasoluna-batch.github.io/guideline/5.0.0.RELEASE/ja/Ch07_JobManagement.html#Ch07_JobManagement_JobStatusManagement)
[初期化時のプロパティ設定など](https://qiita.com/t-yama-3/items/9574f34ccc18de30d625)