MyBatis GeneratorをKotlin + SpringBoot + MySQLで使う方法

# 目的
Springフレームワークで人気のO/R Mapperの「Mybatis」  
Mybatisを利用する際はSQL操作を記述するMapperクラスやデータを格納するModelクラスを定義する必要がありますがそれらのクラスをテーブル毎に自動で生成してくれるツールが「MyBatis Generator」です  
今回はそのツールを利用してKotlinフォーマットで自動生成する方法を書きたいと思います

### メリット
* 単一テーブルに対するCRUD操作を簡潔に行うことができる
* DDL変更に伴うコード修正が簡単
### デメリット
* 複数テーブルに対するRead操作の場合は個別でCustomMapperを作る必要がある
* Kotlinの利点であるNull safetyが利用しづらい

# 環境
* MacBookPro M1(zsh)
* IntelliJ IDEA Community版
* Docker Desktop(ローカルにMySQL8.0を立てるため)

成果物  
GitHub: https://github.com/seki-shinnosuke/kotlin-mybatis-generator-sample

# 内容
やることは大きく3つです

1. ローカルに自動生成したいDBを立てる
2. Gradle(ビルドツール)に依存関係と自動生成タスクを記載
3. Configファイル(XML)を記載

## 1. ローカルに自動生成したいDBを立てる
DDLは同じプロジェクト内で管理することをお勧めします  
(今回の場合はddlフォルダに.sqlファイルを置きDocker初回実行と同時にDBを作ってくれるようにしています)  
```yaml:docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0.30-oracle
    container_name: db
    networks:
      - sample-network
    environment:
      MYSQL_ROOT_PASSWORD: root
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "33306:3306"
    volumes:
      - ./db/ddl:/docker-entrypoint-initdb.d
      - ./db/mysql_data:/var/lib/mysql
networks:
  sample-network:
    driver: bridge
```
```zsh
% docker-compose up -d
```

## 2. Gradle(ビルドツール)に依存関係と自動生成タスクを記載
記載する依存関係とタスクは以下のような感じです  
```kotlin
val mybatisGenerator: Configuration by configurations.creating
dependencies {
    mybatisGenerator("org.mybatis.generator:mybatis-generator-core:1.4.1")
    mybatisGenerator("mysql:mysql-connector-java:8.0.30")
}
task("mybatisGenerator") {
    doLast {
        ant.withGroovyBuilder {
            "taskdef"("name" to "mbgenerator", "classname" to "org.mybatis.generator.ant.GeneratorAntTask", "classpath" to mybatisGenerator.asPath)
        }
        ant.withGroovyBuilder {
            "mbgenerator"("overwrite" to true, "configfile" to "${rootDir}/db/mybatis/generatorConfig.xml", "verbose" to true)
        }
    }
}
```
SpringBootと掛け合わせて利用したい場合はアプリケーション側の依存関係に以下を追加する必要があります  
```kotlin
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2")
implementation("org.mybatis.dynamic-sql:mybatis-dynamic-sql:1.4.0")
```

最終的なbuild.gradle.ktsは以下のようになります
```kotlin:build.gradle.kts
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

object Versions {
    const val jdk = "17"
}

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

allprojects {
    group = "com.example"

    repositories {
        mavenCentral()
        maven("https://plugins.gradle.org/m2/")
    }

    tasks {
        // JSR 305チェックを明示的に有効にする
        withType<KotlinCompile>().configureEach {
            kotlinOptions.freeCompilerArgs = listOf("-Xjsr305=strict", "-java-parameters")
            kotlinOptions.jvmTarget = Versions.jdk
        }
    }
}

apply {
    plugin("kotlin")
    plugin("kotlin-spring")
    plugin("org.springframework.boot")
    plugin("io.spring.dependency-management")
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin")
    implementation("org.springframework:spring-web")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    // アプリ側でMybatisを利用するためのLibrary
    implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2")
    implementation("org.mybatis.dynamic-sql:mybatis-dynamic-sql:1.4.0")

    runtimeOnly("mysql:mysql-connector-java")

    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}
// SpringBootBOMを使用して依存関係のバージョン管理
configure<DependencyManagementExtension> {
    imports {
        mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
    }
}

/**
 * MyBatisGenerator設定
 */
val mybatisGenerator: Configuration by configurations.creating
dependencies {
    // mybatis-generator-coreはver1.4.0->1.4.1でファイル名のprefixやDynamicSQLの記述方法に変更があったため選定は注意
    mybatisGenerator("org.mybatis.generator:mybatis-generator-core:1.4.1")
    mybatisGenerator("mysql:mysql-connector-java:8.0.30")
}
task("mybatisGenerator") {
    doLast {
        ant.withGroovyBuilder {
            "taskdef"("name" to "mbgenerator", "classname" to "org.mybatis.generator.ant.GeneratorAntTask", "classpath" to mybatisGenerator.asPath)
        }
        ant.withGroovyBuilder {
            "mbgenerator"("overwrite" to true, "configfile" to "${rootDir}/db/mybatis/generatorConfig.xml", "verbose" to true)
        }
    }
}
```

## 3. Configファイル(XML)を記載
今回はプロジェクト内にdb/mybatisフォルダを作成しgeneratorConfig.xmlという名前で定義します
```xml:generatorConfig.xml
<?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 >
    <!-- Kotlinで出力するように設定 -->
    <context id="MySQLTables" targetRuntime="MyBatis3Kotlin">
        <!-- ファイルのDocコメントに日時が書き込まれると毎回Git差分となってしまうためOFF -->
        <commentGenerator>
            <property name="suppressDate" value="true" />
        </commentGenerator>
        <!-- DBに接続情報 -->
        <jdbcConnection
                driverClass="com.mysql.cj.jdbc.Driver"
                connectionURL="jdbc:mysql://localhost:33306/sample?allowPublicKeyRetrieval=true&amp;useSSL=false"
                userId="sample-user"
                password="password" />
        <javaTypeResolver>
            <property name="useJSR310Types" value="true"/>
        </javaTypeResolver>
        <!-- モデルクラスを生成する設定 -->
        <javaModelGenerator
                targetPackage="com.example.db.model"
                targetProject="src/main/kotlin">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>
        <!-- マッパークラスを生成する設定 -->
        <javaClientGenerator
                targetPackage="com.example.db.mapper"
                targetProject="src/main/kotlin"
                type="XMLMAPPER">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>
        <!-- コードを生成するテーブルを指定 -->
        <table tableName="sample_test">
            <!-- PKがオートインクリメント系の場合はInsert後に利用したモデルクラスの変数に自動でPK値が返却されるように設定 -->
            <generatedKey column="sample_test_id" sqlStatement="MySql" identity="true" />
        </table>
    </context>
</generatorConfiguration>
```

以上で設定は完了です  
あとはGradleタスクを実行するとjavaModelGenerator/javaClientGeneratorで指定したパッケージ配下にファイルが自動生成されます　　
※指定したパッケージ(ディレクトリ)が存在しない場合、MyBatis Generatorではディレクトリ作成をしてくれないため事前に手動で作っておく必要があります
```zsh
% ./gradlew mybatisGenerator
```
![MybatisGenerator.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728120/172e2ce0-8eaa-1a4f-3d33-9869bf918cf6.png)

Kotlin + SpringBootで利用したい場合はこんな感じで利用できます  
```kotlin
@Service
class SampleService(
    private val sampleTestMapper: SampleTestMapper
) {
    /**
     * MyBatisGeneratorで生成したモデルとマッパーを利用してテーブルにデータを登録
     * @return Insertで自動採番されたPK
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Throwable::class])
    fun registerSample(): Int {
        val record = SampleTest(
            personName = "test",
            sampleStatus = SampleStatus.YET.name,
            registerDatetime = LocalDateTime.now(),
            updateDatetime = LocalDateTime.now()
        )
        sampleTestMapper.insertSelective(record)
        return record.sampleTestId!!
    }
}
```

# まとめ
単一テーブルに対してCRUD操作を行うたびにMapperを作るのはそこそこ大変なので個人的にはMyBatis Generatorを重宝しています　　

ただ、MyBatis Generatorで生成されたMapperの特徴としてInsert/Update時に値がNullで設定されている項目はクエリー生成時に除外され更新しないような仕組みになっているためModelクラスに定義されたフィールドはテーブルカラムがNotNull制約であろうとNull許容型になってしまいます  
Generatorで生成時にテーブルのNotNull制約を見てModelクラスもNotNullにするプラグインも検討しましたが  
・Auto Incrementの値もプログラム側で設定しないといけなくなる  
・Update時に更新しなくても良い値がNotNull制約の場合、同じ値を再設定しないといけない  
と痒い所に手が届かない感じのため賛否両論あると思います