Spring BatchでMyBatisを使ってみよう!

今回はSpring Batch(Spring Bootベース)でMyBatisを使う方法を紹介したいと思います。
Spring Batch自体の説明は別の記事で説明したいと思いますので、本記事では割愛します。

今回作成したアプリケーションのソースコード

GitHub - tsuyoz/spring-batch-mybatis
Contribute to tsuyoz/spring-batch-mybatis development by creating an account on GitHub.

バッチ処理のイメージ

本記事で扱うバッチ処理の概要は以下の通りです。

  • DBに登録されている社員マスタに対して毎日一回処理を実行し、誕生日カラムの内容を元に現在の年齢を計算し、年齢が更新されていればレコードを更新する
  • 更新処理にはチャンクモデルを使用する
  • DBアクセスにはMyBatisを使用する

テーブルイメージ

カラム名称タイプ説明
idIDINTEGER PK
name名前VARCHAR(100)社員名
birthday誕生日DATE
age年齢INTEGER
updated_at更新日時TIMESTAMPレコードの更新日時

環境

Spring Boot2.5.6
Kotlin1.5.31
mybatis-spring-boot-starter2.2.0

データベースはインメモリデータベースであるH2を使用しています。

アプリケーションの実装

それではアプリケーションを実装していきましょう。

アプリケーション設定

まずはgradleの設定を行います。

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.5.6"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.5.31"
    kotlin("plugin.spring") version "1.5.31"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-batch") // (1) Spring Batch 
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0") // (2) MyBatis
    runtimeOnly("com.h2database:h2") // (3) H2
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.batch:spring-batch-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

(1) Spring Batchが利用できるようにライブラリを追加
(2) MyBatisが利用できるようにライブラリを追加
(3) データベースにH2を利用できるようにライブラリを追加

続いてH2データベースの接続設定を行います。

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test
    username: sa
    password:
logging:
  level:
    com:
      example:
        springbatchmybatis:
          da:
            mapper: debug

また、MyBatisで実行されるSQLの内容をログに出力できるように設定を行っています。

データベースの準備

ここではアプリケーションの起動時にH2データベースへ社員マスタテーブルの作成とそのレコードの登録を行えるようにします。
Spring Bootアプリケーションではsrc/resources直下にschema.sql(テーブル作成などを行う)、data.sql(レコード登録を行う)ファイルを配置することで、アプリケーションの起動時にSQLを自動で実行することができます。

-- 社員マスタテーブルの作成
create table EMPLOYEE (
    ID INTEGER NOT NULL PRIMARY KEY,
    NAME VARCHAR(100) NOT NULL,
    BIRTHDAY DATE NOT NULL,
    AGE INTEGER NOT NULL,
    UPDATED_AT TIMESTAMP
);
INSERT INTO EMPLOYEE VALUES(1, '山田 太一', '1997-11-10', 22, null);
INSERT INTO EMPLOYEE VALUES(2, '佐藤 愛', '1995-04-02', 25, null);
INSERT INTO EMPLOYEE VALUES(3, '時任 桜', '1996-06-24', 25, null);

データアクセス処理の実装(MyBatis)

次にMyBatisを使ったデータアクセス処理を実装していきます。

初めに今回扱う社員マスタテーブルを扱うEmployeeクラスを実装します。

package com.example.springbatchmybatis.da.entity

import java.time.LocalDate
import java.time.LocalDateTime

data class Employee(
    var id: Int,
    var name: String,
    var birthday: LocalDate,
    var age: Int,
    var updatedAt: LocalDateTime?,
)

次に実行するSQLを定義するためのマッパーXMLを実装します。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springbatchmybatis.da.mapper.EmployeeMapper">
    <resultMap id="employeeResultMap" type="com.example.springbatchmybatis.da.entity.Employee">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="birthday" property="birthday" jdbcType="DATE"/>
        <result column="age" property="age" jdbcType="INTEGER"/>
        <result column="updated_at" property="updatedAt" jdbcType="TIMESTAMP"/>
    </resultMap>

    <select id="selectAll" resultMap="employeeResultMap">
        select id, name, birthday, age, updated_at
        from employee
        order by id
    </select>

    <update id="updateAge" parameterType="com.example.springbatchmybatis.da.entity.Employee">
        update employee
        set age = #{age},
            updated_at = #{updatedAt}
        where id = #{id}
    </update>
</mapper>

ここではresultMapの設定でEmployeeクラスとの紐付けを行い、社員マスタのレコードを全件取得するためのselectAllと対象の社員の年齢を更新するためのupdateAgeの2つのSQLの定義を行っています。

次にマッパーXMLをJava(Kotlin)から呼び出すためのインターフェースを実装します。

package com.example.springbatchmybatis.da.mapper

import com.example.springbatchmybatis.da.entity.Employee
import org.apache.ibatis.annotations.Mapper

@Mapper
interface EmployeeMapper {
    fun selectAll(): List<Employee>
    fun updateAge(employee: Employee)
}

インターフェース内の2つのメソッドがXMLマッパーのSQLの定義と紐付いています。

バッチ処理の定義

それではバッチ処理の実装を進めて行きましょう。
バッチ処理の定義にはSpring BootのJavaConfigにBean定義を行うことで行います。

package com.example.springbatchmybatis.updateage

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing
import org.springframework.context.annotation.Configuration

@Configuration
@EnableBatchProcessing
class BatchConfig {

バッチ処理定義用の設定クラスを作成し、@Configurationアノテーションを付与します。同時にバッチ処理を有効にするために@EnableBatchProcessingアノテーションを付与します。

チャンクモデルの実装

社員マスタの更新を行うためのチャンクモデルを実装します。
チャンクモデルは処理対象のデータを読み込むためのItemReaderと読み取ったデータを処理するためのItemProcessor、更新処理を行うためのItemWriterで構成されています。
今回のバッチではItemReaderでMyBatisを使いデータベースからレコードを読み込み、ItemProcessorで年齢計算を行い、ItemWriterでMyBatisを使ってデータベースのレコードを更新します。

ItemReaderの実装

BatchConfigクラスにItemReaderの定義を行います。

@Configuration
@EnableBatchProcessing
class BatchConfig(
    private val sqlSessionFactory: SqlSessionFactory, // Autowire
) {

    @Bean
    fun reader(): MyBatisCursorItemReader<Employee> {
        return MyBatisCursorItemReaderBuilder<Employee>()
            .sqlSessionFactory(sqlSessionFactory) 
            .queryId("com.example.springbatchmybatis.da.mapper.EmployeeMapper.selectAll")
            .build()
    }

MyBatisCursorItemReaderはその名の通り、カーソルを使ったデータ取得を行うためのクラスです。
ここではsqlSessionFactoryメソッドでMyBatisのSqlSessionFactoryを設定し、queryIdで実行するMyBatisのXMLマッパー内のSQLのIDを指定しています。

ItemProcessorの実装

次にItemReaderで読み込んだEmployeeクラスを処理するためのItemProcessorを実装します。

package com.example.springbatchmybatis.updateage

import com.example.springbatchmybatis.da.entity.Employee
import org.springframework.batch.item.ItemProcessor
import org.springframework.stereotype.Service
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit

@Service
class UpdateAgeProcessor: ItemProcessor<Employee, Employee> {
    override fun process(employee: Employee): Employee? {

        // 年齢計算
        val age = ChronoUnit.YEARS.between(employee.birthday, LocalDate.now()).toInt()

        // 変更の必要がない場合はスキップ
        if (age == employee.age) return null

        employee.age = age
        employee.updatedAt = LocalDateTime.now()
        return employee
    }
}

ItemProcessorに指定するジェネリクス型についてですが、1つめの型はItemReaderから渡されるクラスの型となり、2つめはItemWriterに渡すクラスの型となります。今回の例ではどちらもEmployee型となります。
ItemProcessorでは引数としてItemReaderからの値を受け取り、戻り値として返した値がItemWriteに渡されます。また、戻り値にNULLを返すことで後続のItemWriteへの処理をキャンセルすることができます。今回の例では年齢が更新されない社員の場合にNULLを返却するようにしています。

ItemWriterの実装

次にItemWriterの定義を行います。

    @Bean
    fun writer(): MyBatisBatchItemWriter<Employee> {
        return MyBatisBatchItemWriterBuilder<Employee>()
            .sqlSessionFactory(sqlSessionFactory)
            .statementId("com.example.springbatchmybatis.da.mapper.EmployeeMapper.updateAge")
            .build()
    }

ItemReaderと同じようにsqlSessionFactoryでMyBatisのSqlSessionFactoryを設定し、statementIdで実行するMyBatisのXMLマッパー内のSQLのIDを指定しています。

Stepの定義

BatchConfigクラスに先程定義したItemReader, Processor, Writerを使いStepを定義します。

@Configuration
@EnableBatchProcessing
class BatchConfig(
    private val jobBuilderFactory: JobBuilderFactory,
    private val stepBuilderFactory: StepBuilderFactory,
    private val sqlSessionFactory: SqlSessionFactory,
    private val updateAgeProcessor: UpdateAgeProcessor,
) {

    // (省略)

    @Bean
    fun updateStep(): Step {
        return stepBuilderFactory.get("update-age-update-step") // step名を指定
            .chunk<Employee, Employee>(10) // チャンクサイズを設定
            .reader(reader()) // ItemReaderを設定
            .processor(updateAgeProcessor) // ItemProcessorを指定
            .writer(writer()) // ItemWriterを設定
            .build()
    }

バッチ処理の実行

ここまででバッチ処理が完成しましたので、処理を実行してみましょう!

$ ./gradlew bootRun
 .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.6)

2021-11-03 16:00:32.618  INFO 3811 --- [           main] c.e.s.SpringBatchMybatisApplicationKt    : Starting SpringBatchMybatisApplicationKt using Java 11.0.12 on yaris.local with PID 3811 (/XXX/spring-batch-mybatis/build/classes/kotlin/main started by XXX in /XXX/spring-batch-mybatis)
2021-11-03 16:00:32.619  INFO 3811 --- [           main] c.e.s.SpringBatchMybatisApplicationKt    : No active profile set, falling back to default profiles: default
2021-11-03 16:00:33.942  INFO 3811 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-11-03 16:00:34.037  INFO 3811 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2021-11-03 16:00:34.217  INFO 3811 --- [           main] o.s.b.c.r.s.JobRepositoryFactoryBean     : No database type set, using meta data indicating: H2
2021-11-03 16:00:34.270  INFO 3811 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : No TaskExecutor has been set, defaulting to synchronous executor.
2021-11-03 16:00:34.375  INFO 3811 --- [           main] c.e.s.SpringBatchMybatisApplicationKt    : Started SpringBatchMybatisApplicationKt in 2.369 seconds (JVM running for 2.739)
2021-11-03 16:00:34.377  INFO 3811 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: []
2021-11-03 16:00:34.407  INFO 3811 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=update-age-job]] launched with the following parameters: [{}]
2021-11-03 16:00:34.427  INFO 3811 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [update-age-update-step]
2021-11-03 16:00:34.435 DEBUG 3811 --- [           main] c.e.s.d.mapper.EmployeeMapper.selectAll  : ==>  Preparing: select id, name, birthday, age, updated_at from employee order by id
2021-11-03 16:00:34.442 DEBUG 3811 --- [           main] c.e.s.d.mapper.EmployeeMapper.selectAll  : ==> Parameters: 
2021-11-03 16:00:34.458 DEBUG 3811 --- [           main] c.e.s.d.mapper.EmployeeMapper.selectAll  : <==      Total: 3
2021-11-03 16:00:34.461 DEBUG 3811 --- [           main] c.e.s.d.mapper.EmployeeMapper.updateAge  : ==>  Preparing: update employee set age = ?, updated_at = ? where id = ?
2021-11-03 16:00:34.465 DEBUG 3811 --- [           main] c.e.s.d.mapper.EmployeeMapper.updateAge  : ==> Parameters: 23(Integer), 2021-11-03T16:00:34.459750(LocalDateTime), 1(Integer)
2021-11-03 16:00:34.468 DEBUG 3811 --- [           main] c.e.s.d.mapper.EmployeeMapper.updateAge  : ==> Parameters: 26(Integer), 2021-11-03T16:00:34.459814(LocalDateTime), 2(Integer)
2021-11-03 16:00:34.476  INFO 3811 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [update-age-update-step] executed in 49ms
2021-11-03 16:00:34.481  INFO 3811 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=update-age-job]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 64ms
2021-11-03 16:00:34.486  INFO 3811 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2021-11-03 16:00:34.490  INFO 3811 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

ちょっと分かりづらいですが、バッチ処理が実行されて2件のレコードが更新されています。

まとめ

いかがだったでしょうか。今回はSpring BatchとMyBatisの連携方法を簡単に紹介させてもらいました。
Spring Batchには他にも分散処理(Remote Chunking、Remote Partitioning)やリトライ処理など様々な機能があり、複雑なバッチ処理を行う上で非常に有用な製品だと思います。今後このあたりの機能についても別の記事で紹介できればと考えています。

最後までお読みいただき、ありがとうございました。

コメント

タイトルとURLをコピーしました