Javaで人気のSpringFrameworkの機能の一つであるSpringBatchを使ったコードを業務で書いたので、紹介と復習を兼ねて自宅でも実装しておこうと思う。最終的なコードはGitHubに置いてある。
環境
- Java10
- CentOS7
- MySQL5.7
作るもの
業務で書いたコードを載せるわけにはいかないので、代わりになるものを作ろうと思う。とある個人テーブルの年齢データを、生年月日を見て正しい年齢に修正するバッチを作ることにする。
データ
まずはデータを用意する。VagrantでCentOS7を起動して、そこにMySQLをインストールします。batchというDBを作って下記のようなテーブルを準備します。今回は年齢と生年月日があれば良いのですが、それだけだと寂しいので名前も用意しました。ちなみにバッチは日次で実行される想定です。
mysql> use batch;
Database changed
mysql> desc person;
+----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(30) | NO | | NULL | |
| age | int(3) | NO | | NULL | |
| birthday | date | NO | | NULL | |
+----------+-------------+------+-----+---------+----------------+
4人分ほどデータを入れます。本日は8月29日なので、西暦だけ違う誕生日のデータを入れます。余計に更新していないかわかるように一件は別日の誕生日で用意します。
mysql> select * from person;
+----+-------+-----+------------+
| id | name | age | birthday |
+----+-------+-----+------------+
| 1 | Alice | 23 | 1995-08-29 |
| 2 | Bob | 38 | 1980-08-29 |
| 3 | Carol | 29 | 1989-08-29 |
| 4 | Dave | 23 | 1995-08-30 |
+----+-------+-----+------------+
Javaコンポーネント
SpringFrameworkを使う時はSpring Initializrから雛形をダウンロードすると早いので、こちらを使います。ビルドはGradleを使うのでGradleProjectを選択し、あとは適当に入力していきます。
Dependenciesには
- Spring Batch
- Lombok
- MyBatis Framework
- MySQL Driver
あたりを適当に選択して「Generate the project」でダウンロードし解凍します。これに必要なファイルを加えていきます。SpringBatchのアーキテクチャについてはこちらに詳しく書いてあるので、一読されると良いかと思います。
SpringBatchでは、バッチの実行単位をJOB、処理の単位をSTEPとして定義します。また、STEPの処理フローの雛形としてタスクレットモデルとチャンクモデルという二つの概念が用意されています。本来単一のテーブルを参照更新するだけであればタスクレットで充分だと思いますが、今回は敢えてチャンクモデルを採用して実装します。
チャンクモデルは「読み込み→加工→書き込み」の三つフローに分けて実装する必要があり、それぞれインタフェースが用意されています。
インタフェース | 実装内容 |
---|---|
ItemReader | DBから、実行日の月日と同じの月日の生年月日を持つ個人データを抽出する。 |
ItemProcessor | 生年月日から年齢を算出し、年齢を更新した個人データを作成する。 |
ItemWriter | 作成した個人データをDBに書き込む。 |
データの読み書き
データの読み書きではDBに接続します。MyBatisのライブラリからそれぞれのインタフェースの実装クラスが提供されていますから、そちらを使うこととします。読み込み(ItemReader)では、バッチ実行日と同じ月日を誕生日とする個人レコードを抽出します。なので下記のようなSQLを発行しようと思います。当日(today)の日付はアプリから渡します。
SELECT
id,
name,
age,
birthday
FROM
person
WHERE
Date_format(birthday, '%m%d') = Date_format(#{today}, '%m%d')
書き込み(ItemWriter)は年齢を更新するだけですね。
update
person
set
age = #{age}
where
id = #{id}
データの加工
データの加工(ItemProcessor)では、抽出した個人データオブジェクトの年齢を正しく修正することが責務となります。と言っても今年と誕生年の差を算出して更新用オブジェクトを作るだけですね。
// import省略
@Component
@Slf4j
public class CorrectAgeProcessor implements ItemProcessor<Person, Person> {
@Override
public Person process(Person person) throws Exception {
log.info("Correct {}.", person.getName());
return new Person(
person.getId(),
person.getName(),
LocalDate.now().getYear() - person.getBirthday().getYear(),
person.getBirthday());
}
}
ジョブ構成の定義
ジョブ構成の定義をします。今回はステップは一つで充分なのでstep
というBeanを一つ用意します。SpringBatchはデフォルトでトランザクション処理を実施してくれるそうで、chunk(n)
で設定した件数間隔でコミットをしてくれます。ある程度まとめてコミットすることで、コミット時のオーバーヘッドを削減することが目的ですね。今回はとりあえず一件ごとにコミットします。
// import省略
@Configuration
@EnableBatchProcessing
@RequiredArgsConstructor
public class BatchConfiguration {
public final JobBuilderFactory jobBuilderFactory;
public final StepBuilderFactory stepBuilderFactory;
private final SqlSessionFactory sqlSessionFactory;
private final CorrectAgeProcessor correctAgeProcessor;
@Bean
public MyBatisCursorItemReader<Person> reader() {
Map<String, Object> params = new HashMap<>();
params.put("today", LocalDate.now());
return new MyBatisCursorItemReaderBuilder<Person>()
.sqlSessionFactory(sqlSessionFactory)
.queryId("com.github.hysrabbit.agecorrector.mybatis.mapper.PersonMapper.findByBirthday")
.parameterValues(params)
.build();
}
@Bean
public MyBatisBatchItemWriter<Person> writer() {
return new MyBatisBatchItemWriterBuilder<Person>()
.sqlSessionFactory(sqlSessionFactory)
.statementId("com.github.hysrabbit.agecorrector.mybatis.mapper.PersonMapper.save")
.build();
}
@Bean
public Job correctAge(JobListener jobListener, Step step) {
return jobBuilderFactory.get("correctAge")
.incrementer(new RunIdIncrementer())
.listener(jobListener)
.flow(step)
.end()
.build();
}
@Bean
public Step step(ItemReader<Person> reader, ItemWriter<Person> writer) {
return stepBuilderFactory.get("step")
.<Person, Person> chunk(1)
.reader(reader)
.processor(correctAgeProcessor)
.writer(writer)
.build();
}
}
その他もろもろ実装してこちらに最終的なコードをプッシュしてあります。テストコードなどは未実装なので、そのうち実装しようかと思います。
実行はMySQLをインストールしたVM上で行います。実行前に、DBの設定情報を環境変数に定義します。
$ export SPRING_DATASOURCE_URL=jdbc:mysql://<hostname>:<port>/<database>;
$ export SPRING_DATASOURCE_USERNAME=<username>;
$ export SPRING_DATASOURCE_PASSWORD=<password>;
その後Gradleでビルドして、できたJarファイルを実行します。挿入したloggerで、更新した人の名前が出力されていますね。
$ ./gradlew clean build
.
.
.
$ java -jar build/libs/agecorrector.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.7.RELEASE)
.
.
.
2019-08-29 01:50:29.334 INFO 2781 --- [ main] c.g.h.agecorrector.batch.JobListener : Start job.
2019-08-29 01:50:29.391 INFO 2781 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step]
2019-08-29 01:50:29.565 INFO 2781 --- [ main] c.g.h.a.batch.CorrectAgeProcessor : Correct Alice.
2019-08-29 01:50:29.609 INFO 2781 --- [ main] c.g.h.a.batch.CorrectAgeProcessor : Correct Bob.
2019-08-29 01:50:29.624 INFO 2781 --- [ main] c.g.h.a.batch.CorrectAgeProcessor : Correct Carol.
2019-08-29 01:50:29.651 INFO 2781 --- [ main] c.g.h.agecorrector.batch.JobListener : Completed job.
.
.
.
MySQLでもデータを確認してみます。実行した日は8/29
でしたが、その日に生まれた人の年齢を更新することができていますね。
mysql> select * from person;
+----+-------+-----+------------+
| id | name | age | birthday |
+----+-------+-----+------------+
| 1 | Alice | 24 | 1995-08-29 |
| 2 | Bob | 39 | 1980-08-29 |
| 3 | Carol | 30 | 1989-08-29 |
| 4 | Dave | 23 | 1995-08-30 |
+----+-------+-----+------------+
まとめ
あとはCronなどに登録しておけば年齢を定期更新できます。SpringBatchとMyBatisの組み合わせはJavaプログラマにとって理解しやすく扱いやすいフレームワークだと思います。また、今回実装していない便利な機能などSpringBatchには多く存在するので、是非とも利用してみてください。