目的
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つです
- ローカルに自動生成したいDBを立てる
- Gradle(ビルドツール)に依存関係と自動生成タスクを記載
- Configファイル(XML)を記載
1. ローカルに自動生成したいDBを立てる
DDLは同じプロジェクト内で管理することをお勧めします
(今回の場合はddlフォルダに.sqlファイルを置きDocker初回実行と同時にDBを作ってくれるようにしています)
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
% docker-compose up -d
2. Gradle(ビルドツール)に依存関係と自動生成タスクを記載
記載する依存関係とタスクは以下のような感じです
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と掛け合わせて利用したい場合はアプリケーション側の依存関係に以下を追加する必要があります
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は以下のようになります
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 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&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ではディレクトリ作成をしてくれないため事前に手動で作っておく必要があります
% ./gradlew mybatisGenerator
Kotlin + SpringBootで利用したい場合はこんな感じで利用できます
@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制約の場合、同じ値を再設定しないといけない
と痒い所に手が届かない感じのため賛否両論あると思います