【MyBatis Generator 1.4.2】Spring Boot3でMyBatis Dynamic SQLを使用する

2023年11月18日

概要

MyBatis Generatorの公式ページWhat’s New in MyBatis Generatorにあるように、バージョン1.4.0以降ではデフォルトのランタイムがMyBatis Dynamic SQLになりました。

以前このブログに書いたMyBatis Generator 1.3.7の検証記事Spring BootでMyBatis Generator のGradleプラグインを使用するにあるような従来のExampleが廃止され、ラムダを使ってSQLを組み立てる形式となりました。

この記事では以下の環境でMyBatis Generator Dynamic SQLを使う場合の設定を紹介します。

  • Spring Boot 3.1.5
  • Java 17
  • PostgreSQL 15.4
  • MyBatis-Generator-Core 1.4.2

build.gradleへ存関係を追加

Spring Bootプロジェクトのbuild.gradleへMyBatis Generatorプラグインと、MyBatis Generator本体であるMyBatis Generator Coreへの依存関係を追加します。

また、Spring BootでMyBatisを使用するときにはMyBatis-Spring-Boot-Starterを使用すると設定が簡単になるのであわせてbuild.gradleへ追加します。

build.gradle中のgeneratorConfig.xmlがMyBatist Generatorの設定ファイルです。Spring Bootプロジェクト内に追加する必要があります。

Gradle Mybatis Generatorプラグインの設定はkimichen13 / mybatis-generator-pluginを参考にしています。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.5'
	id 'io.spring.dependency-management' version '1.1.3'
        // MyBatis Generatorプラグイン
	id 'com.qqviaja.gradle.MybatisGenerator' version '2.5'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
        // MyBatis Generator
	mybatisGenerator
}

// MyBatis Generatorの設定
mybatisGenerator {
	verbose = true
        // 設定ファイルのパス
	configFile = "src/main/resources/generatorConfig.xml"
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'

    // MyBatis
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2'
	implementation group: 'org.mybatis.generator', name: 'mybatis-generator-core', version: '1.4.2'
    implementation group: 'org.mybatis.dynamic-sql', name: 'mybatis-dynamic-sql', version: '1.5.0'

	implementation group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'org.postgresql:postgresql'
	annotationProcessor 'org.projectlombok:lombok'

    testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.2'

    // MyBatisマッパーのテスト用
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	
}

tasks.named('test') {
	useJUnitPlatform()
}

MyBatis Generatorの設定ファイルを作成

generatorConfig.xmlを作成します。このファイルにはMyBatis Generator自体の設定と接続先DB、MyBatis Generatorの実行対象テーブルを記述します。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration >
    <context id="context1" >

        <!-- 生成される Java ファイルのコメントに日付を付与しない -->
        <commentGenerator>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <!-- 生成元DBの接続情報 -->
        <jdbcConnection driverClass="org.postgresql.Driver" connectionURL="jdbc:postgresql://localhost:5432/TestDB"
                        userId="xxxxx" password="xxxxx"/>
        <!-- JSR310の型を使用する  -->
        <javaTypeResolver>
            <property name="useJSR310Types" value="true"/>
        </javaTypeResolver>

        <!-- 出力先ディレクトリ設定 -->
        <javaModelGenerator targetPackage="com.example" targetProject="src/main/java"/>
        <javaClientGenerator targetPackage="com.example" targetProject="src/main/java"/>

        <table tableName="book">
            <!-- 自動生成されるキー -->
            <generatedKey column="id" identity="true" sqlStatement="JDBC"/>

            <!-- PostgreSQLのtimestamp with timezoneをJavaのOffsetDateTimeへマッピングする -->
            <columnOverride column="updated_at" jdbcType="TIMESTAMP_WITH_TIMEZONE" javaType="java.time.OffsetDateTime" />
        </table>
    </context>

</generatorConfiguration>

動作確認

下記のDDLで作成したbookテーブルに対して上記のgeneratorConfig.xmlの設定でMyBatis Generatorを実行してみます。

create table book (
    id serial not null primary key,
    book_title text not null,
    category_id integer not null,
    updated_at timestamp with time zone not null
);

生成されるファイルは以下の3つです。

  • Book.java(エンティティクラス)
  • BookDynamicSqlSupport.java (テーブル、カラム情報)
  • BookMapper.java (マッパークラス)

Book.javaは以下のようになります。

package com.example.demo;

import java.time.OffsetDateTime;
import javax.annotation.Generated;

public class Book {
    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    private Integer id;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    private String bookTitle;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    private Integer categoryId;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    private OffsetDateTime updatedAt;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public Integer getId() {
        return id;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public void setId(Integer id) {
        this.id = id;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public String getBookTitle() {
        return bookTitle;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public void setBookTitle(String bookTitle) {
        this.bookTitle = bookTitle;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public Integer getCategoryId() {
        return categoryId;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public void setCategoryId(Integer categoryId) {
        this.categoryId = categoryId;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public OffsetDateTime getUpdatedAt() {
        return updatedAt;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public void setUpdatedAt(OffsetDateTime updatedAt) {
        this.updatedAt = updatedAt;
    }
}

BookDynamicSqlSupport.javaは以下のようになります。

package com.example.demo;

import java.sql.JDBCType;
import java.time.OffsetDateTime;
import javax.annotation.Generated;
import org.mybatis.dynamic.sql.SqlColumn;
import org.mybatis.dynamic.sql.SqlTable;

public final class BookDynamicSqlSupport {
    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final Book book = new Book();

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<Integer> id = book.id;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<String> bookTitle = book.bookTitle;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<Integer> categoryId = book.categoryId;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<OffsetDateTime> updatedAt = book.updatedAt;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final class Book extends SqlTable {
        public final SqlColumn<Integer> id = column("id", JDBCType.INTEGER);

        public final SqlColumn<String> bookTitle = column("book_title", JDBCType.VARCHAR);

        public final SqlColumn<Integer> categoryId = column("category_id", JDBCType.INTEGER);

        public final SqlColumn<OffsetDateTime> updatedAt = column("updated_at", JDBCType.TIMESTAMP_WITH_TIMEZONE);

        public Book() {
            super("book");
        }
    }
}

BookMapper.javaは以下のようになります。

package com.example.demo;

import static com.example.demo.BookDynamicSqlSupport.*;
import static org.mybatis.dynamic.sql.SqlBuilder.*;

import com.example.demo.Book;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import javax.annotation.Generated;
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.ResultMap;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.dynamic.sql.BasicColumn;
import org.mybatis.dynamic.sql.delete.DeleteDSLCompleter;
import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider;
import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider;
import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider;
import org.mybatis.dynamic.sql.select.CountDSLCompleter;
import org.mybatis.dynamic.sql.select.SelectDSLCompleter;
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.mybatis.dynamic.sql.update.UpdateDSL;
import org.mybatis.dynamic.sql.update.UpdateDSLCompleter;
import org.mybatis.dynamic.sql.update.UpdateModel;
import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider;
import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;

@Mapper
public interface BookMapper {
    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    BasicColumn[] selectList = BasicColumn.columnList(id, bookTitle, categoryId, updatedAt);

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    @SelectProvider(type=SqlProviderAdapter.class, method="select")
    long count(SelectStatementProvider selectStatement);

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    @DeleteProvider(type=SqlProviderAdapter.class, method="delete")
    int delete(DeleteStatementProvider deleteStatement);

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    @InsertProvider(type=SqlProviderAdapter.class, method="insert")
    @Options(useGeneratedKeys=true,keyProperty="record.id")
    int insert(InsertStatementProvider<Book> insertStatement);

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    @Insert({
        "${insertStatement}"
    })
    @Options(useGeneratedKeys=true,keyProperty="records.id")
    int insertMultiple(@Param("insertStatement") String insertStatement, @Param("records") List<Book> records);

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default int insertMultiple(MultiRowInsertStatementProvider<Book> multipleInsertStatement) {
        return insertMultiple(multipleInsertStatement.getInsertStatement(), multipleInsertStatement.getRecords());
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    @SelectProvider(type=SqlProviderAdapter.class, method="select")
    @ResultMap("BookResult")
    Optional<Book> selectOne(SelectStatementProvider selectStatement);

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    @SelectProvider(type=SqlProviderAdapter.class, method="select")
    @Results(id="BookResult", value = {
        @Result(column="id", property="id", jdbcType=JdbcType.INTEGER, id=true),
        @Result(column="book_title", property="bookTitle", jdbcType=JdbcType.VARCHAR),
        @Result(column="category_id", property="categoryId", jdbcType=JdbcType.INTEGER),
        @Result(column="updated_at", property="updatedAt", jdbcType=JdbcType.TIMESTAMP_WITH_TIMEZONE)
    })
    List<Book> selectMany(SelectStatementProvider selectStatement);

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    @UpdateProvider(type=SqlProviderAdapter.class, method="update")
    int update(UpdateStatementProvider updateStatement);

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default long count(CountDSLCompleter completer) {
        return MyBatis3Utils.countFrom(this::count, book, completer);
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default int delete(DeleteDSLCompleter completer) {
        return MyBatis3Utils.deleteFrom(this::delete, book, completer);
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default int deleteByPrimaryKey(Integer id_) {
        return delete(c -> 
            c.where(id, isEqualTo(id_))
        );
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default int insert(Book record) {
        return MyBatis3Utils.insert(this::insert, record, book, c ->
            c.map(bookTitle).toProperty("bookTitle")
            .map(categoryId).toProperty("categoryId")
            .map(updatedAt).toProperty("updatedAt")
        );
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default int insertMultiple(Collection<Book> records) {
        return MyBatis3Utils.insertMultiple(this::insertMultiple, records, book, c ->
            c.map(bookTitle).toProperty("bookTitle")
            .map(categoryId).toProperty("categoryId")
            .map(updatedAt).toProperty("updatedAt")
        );
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default int insertSelective(Book record) {
        return MyBatis3Utils.insert(this::insert, record, book, c ->
            c.map(bookTitle).toPropertyWhenPresent("bookTitle", record::getBookTitle)
            .map(categoryId).toPropertyWhenPresent("categoryId", record::getCategoryId)
            .map(updatedAt).toPropertyWhenPresent("updatedAt", record::getUpdatedAt)
        );
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default Optional<Book> selectOne(SelectDSLCompleter completer) {
        return MyBatis3Utils.selectOne(this::selectOne, selectList, book, completer);
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default List<Book> select(SelectDSLCompleter completer) {
        return MyBatis3Utils.selectList(this::selectMany, selectList, book, completer);
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default List<Book> selectDistinct(SelectDSLCompleter completer) {
        return MyBatis3Utils.selectDistinct(this::selectMany, selectList, book, completer);
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default Optional<Book> selectByPrimaryKey(Integer id_) {
        return selectOne(c ->
            c.where(id, isEqualTo(id_))
        );
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default int update(UpdateDSLCompleter completer) {
        return MyBatis3Utils.update(this::update, book, completer);
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    static UpdateDSL<UpdateModel> updateAllColumns(Book record, UpdateDSL<UpdateModel> dsl) {
        return dsl.set(bookTitle).equalTo(record::getBookTitle)
                .set(categoryId).equalTo(record::getCategoryId)
                .set(updatedAt).equalTo(record::getUpdatedAt);
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    static UpdateDSL<UpdateModel> updateSelectiveColumns(Book record, UpdateDSL<UpdateModel> dsl) {
        return dsl.set(bookTitle).equalToWhenPresent(record::getBookTitle)
                .set(categoryId).equalToWhenPresent(record::getCategoryId)
                .set(updatedAt).equalToWhenPresent(record::getUpdatedAt);
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default int updateByPrimaryKey(Book record) {
        return update(c ->
            c.set(bookTitle).equalTo(record::getBookTitle)
            .set(categoryId).equalTo(record::getCategoryId)
            .set(updatedAt).equalTo(record::getUpdatedAt)
            .where(id, isEqualTo(record::getId))
        );
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    default int updateByPrimaryKeySelective(Book record) {
        return update(c ->
            c.set(bookTitle).equalToWhenPresent(record::getBookTitle)
            .set(categoryId).equalToWhenPresent(record::getCategoryId)
            .set(updatedAt).equalToWhenPresent(record::getUpdatedAt)
            .where(id, isEqualTo(record::getId))
        );
    }
}

MyBatisマッパーの単体テストの作成方法は以下の記事にまとめています。

Java,PostgreSQL,Spring,SQL

Posted by fanfanta