Spring BootでMybatisを使う with Kotlin

# はじめに
Spring Bootで、Mybatisを使う際Javaと同じ構成だと動かずハマった...
しかし、「Spring x Kotlin x Mybatis」の構成は、調べてもあまり情報が出てこない。

なんとか、解決できた際のコードを紹介する。

# 目次
- 実行環境
- 設定していく
    - 依存ライブラリの追加
    - RDBへの接続プロパティを記述する
    - プロパティ保持クラスを用意する
    - セッションファクトリ、セッションテンプレートのBeanを定義する
    - SQLを発行し、結果を得る
- おわりに
    - KotlinのORMについて
    - Mybatis Generatorについて
- 参考

# 実行環境
Mybatisは、Spring拡張のものを使う。

| ライブラリ | バージョン |
|:-:|:-:|
| Kotlin/JVM  | 1.3.72  |
| Spring Boot | 2.3.3.RELEASE  |
|  Mybatis(mybatis-spring-boot-starter) | 2.1.3  |
|  Mybatis Generator | 1.3.7  |
|  RDB | Oracle Database 18c  |

# 設定していく

準備するものは、ざっくりわけると

- build.gradle.kts
- application.properties
- application.propertiesを動的に読み込むKotlinクラス
- DB接続ソース設定クラス
- Mybatis関連のMapper

`application.properties` の値を動的に読み込み、DataSourceに設定できるようにする。

## 依存ライブラリの追加
なにはなくとも、まずは依存ライブラリから。

```kotlin:build.gradle.kts
dependencies {
	/**
	 * Spring
	 */
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("org.springframework.boot:spring-boot-starter-aop")
	runtimeOnly("org.springframework.boot:spring-boot-starter-tomcat")

	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

	/**
	 * Mybatis、Driver
	 */
	implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3")
	implementation(files("libs/ojdbc8.jar")) // ローカルのDriver jarを追加する
	mybatisGenerator("org.mybatis.generator:mybatis-generator-core:1.3.7") // ここでは細かくは触れないが、テーブル定義からマッパーを自動生成する

	testImplementation("org.springframework.boot:spring-boot-starter-test") {
		exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
	}
}
```

## RDBへの接続プロパティを記述する
application.propertiesに、読み込むプロファイルごとのユーザ名やパスワードを記述する。
ここは実行環境に合わせて書き換える。

```application.properties
spring.datasource.url=jdbc:oracle:thin:@localhost:1521/DB
spring.datasource.username=<username>
spring.datasource.password=<password>
spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver
```

今回は1枚だけだが、テスト・本番と分けたい場合は同じようにして `application-ut.properties` , `application-prod.properties` を用意すればよいです。
 
## プロパティ保持クラスを用意する
アプリケーションを起動した際に、プロファイルを判断して `application.properties` の値を Javaオブジェクトに保持させるクラスを用意する。
このクラスは、Beanをインスタンス化する際に値を詰めるクラスなので、プロパティはすべて `lateinit` な変数としておく。次に用意するセッションファクトリクラス内 `DataSource` インスタンスに設定する。

他のBeanに混ぜて使えるようにするべく、 `@Component` アノテーションを付与し、 `DataSource` を定義するクラスに注入する。
クラスのフィールド名は `application.properties` の項目に揃える。

```kotlin:PropertyHolder.kt
@Component
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
class PropertyHolder {
    lateinit var username: String
    lateinit var password: String
    lateinit var url: String
    lateinit var driverClassName: String
}
```

## セッションファクトリ、セッションテンプレートのBeanを定義する
Mybatisのセッションファクトリ、セッションテンプレートを定義する。
このBean定義により、アプリケーションが起動するとコネクションを張る準備ができる。

```kotlin:SessionFactory.kt
@Configuration
@MapperScan(
        basePackages = ["com.example.api.dao","com.example.api.repository"],
        sqlSessionTemplateRef = "SqlSessionTemplate"
)
class SqlSessionFactory(private val propertyHolder: PropertyHolder) { // ↑で定義したプロパティを持っているクラスを注入する。

    companion object {
        // XMLのありか
        // クラスの外にstaticフィールドとして切り出してもいい
        const val MAPPER_XML_PATH: String = "classpath:com/example/api/**/*.xml" 
    }

    /**
     * データソース
     */
    @Bean(name = ["DataSource"])
    @ConfigurationProperties(prefix = "spring.datasource")
    fun dataSource(): DataSource? {
        // プロパティから読み込んでデータソースにセットする
        return PooledDataSource().apply {
            url = propertyHolder.url
            username = propertyHolder.username
            password = propertyHolder.password
            driver = propertyHolder.driverClassName
        }
    }

    /**
     * SQLセッションファクトリ
     */
    @Bean(name = ["SqlSessionFactory"])
    @Throws(Exception::class)
    fun sqlSessionFactory(@Qualifier("DataSource") dataSource: DataSource?): SqlSessionFactory? {
        val bean: SqlSessionFactoryBean = SqlSessionFactoryBean()
        bean.setDataSource(dataSource)
        bean.setMapperLocations(*PathMatchingResourcePatternResolver().getResources(MAPPER_XML_PATH))
        return bean.getObject()
    }

    /**
     * SQLセッションテンプレート
     */
    @Bean(name = ["SqlSessionTemplate"])
    @Throws(java.lang.Exception::class)
    fun sqlSessionTemplate(@Qualifier("SqlSessionFactory") sqlSessionFactory: SqlSessionFactory?): SqlSessionTemplate? {
        return SqlSessionTemplate(sqlSessionFactory)
    }

}

```

特に、Kotlinで記述する際のポイントは↓ここ。

```kotlin
fun dataSource(): DataSource? {
    // プロパティから読み込んでデータソースにセットする
    return PooledDataSource().apply {
        url = propertyHolder.url
        username = propertyHolder.username
        password = propertyHolder.password
        driver = propertyHolder.driverClassName
    }
}
```
Javaと違い、 `DataSouorce` のコンストラクタに明示的に値を渡してインスタンスをつくらないといけないらしい。なんか違うような気がするが...とにもかくにもこう書くことで乗り切れた。
昔、ほぼ同じコードをJavaでやったことがあり、そのまま移植したらハマった。全然違った...
Javaでは、単に空のコンストラクタでインスタンスを初期化すれば `@ConfigurationProperties(prefix = "spring.datasource")` から勝手に `DataSource` の準備をしてくれていた。
はじめ、これが全然わからなくてMybatisの例外にドハマリしました...この設定方法紹介してる記事も全然なくて、まじでわからなかった...

# SQLを発行し、結果を得る
さて、ここまでで準備が完了。あとは、手書きSQLでも自動生成コードでも、好きなMybatisのマッパーをロジッククラスにDIしてSQLを発行すればいい。

MybatisGeneratorの自動生成コードでいくなら、こんな感じ。
ここでは、 `Customer` というテーブルがRDBにあり、ジェネレータでエンティティ、例オブジェクト、マッパーがあると仮定する。
※本記事の本筋ではないので、インターフェースやら例外処理やらは省き最低限のSQL発行関連のみ記述。


```kotlin:CustomerServiceImpl.kt
class CustomerServiceImpl(private val customerRepository: CustomerRepository) {

    override fun retrieve(req: List<String>): List<Customer> {

        val condition: CustomerExample = CustomerExample()
        condition.createCriteria().andEmailIn(req)

        // クエリを実行する
        val customers: List<Customer> = customerRepository.selectByExample(condition)

        return customers
    }
}
```

# おわりに
## KotlinのORMについて
やや実装を急いでいたため、一から学習するのがためらわれたが、KotlinのORMライブラリとしては [Exposed](https://github.com/JetBrains/Exposed/wiki)が便利で扱いやすそうである。
ActiveRecordのような、ORM然としたライブラリである印象を受ける。

SQLの発行やトランザクション管理などは、DSLによりラップされ、ライブラリが制御をしているのかも。
今回はORMにMybatisを選択したものの、自動生成コードはJavaになってしまうし、なんだかんだxmlを使うことになってカスタマイズが効く一方で設定やらdebugがかったるい。
その点、ExposedはKotlinネイティブ、軽量コンパクトな点はとても魅力的なように思える。

一方で、今回は小さいサービスの実装だったので選択肢としてナシではなかったものの、大きめのサービスを作る場合はSQLをある程度柔軟に扱えるMybatisのほうが気が楽なように思える。
詳しくはExposedのREADMEを参照していただければ。

## Mybatis Generatorについて
Mybatis Generatorは、1.3系と1.4系で自動生成できるコードがかなり異なる。
generator関連の公式ドキュメントがぱっとは見当たらず（未だに見たことがない）、使い慣れた1.3系の状態でコードを生成した。

generator用のGradleビルドスクリプトも、groovyとkotlin DSL結構違った。最適化は多少苦労しました...
ここで書くとまた長くなってしまうので、機会があれば別の記事で。

# 参考

- [Kotlin-MyBatis-Spring 取得結果についての備忘録](https://tech.askul.co.jp/entry/2019/06/07/180826)
-  [Exposed](https://github.com/JetBrains/Exposed/wiki)
-  [Hibernate/JPAからExposed DAOに乗り換えて、継承クラスを扱いやすくした話](https://medium.com/@naka.tomohiro/kotlin-hibernate-to-exposed-ebff29845db4)