Kotlin 1.1とMyBatis 3.4.1+で@Paramを排除できる!!

  • 10
    いいね
  • 0
    コメント

昨日投稿した「Spring Boot 1.5からPlatformTransactionManager用のCustomizerが追加される」が今年の書き納めだと思っていたのですが・・・:sweat_smile: 書きたいことをみつけてしまったので、ほんとに今年最後の投稿をしま〜す :wink:

ネタとしては・・・ 2017 1Q(1〜3月)にリリース予定のKotlin 1.1とMyBatis 3.4.1+(Java SE 8から追加された-parametersオプション使ってコンパイルするとMapperメソッドの引数名がバインド変数名になるバージョン)を組み合わせると、@Paramを使う必要がなくなるよ〜という話です。

Note:

2017/03/02
Kotlin 1.1が正式にリリースされたので、リリース版ベースに全面的に修正しました!!(内容は基本一緒です)

Kotlin 1.0の場合は?

Kotilin 1.0.xの場合は・・・

@Select("""
    SELECT
        id, title, details, finished
    FROM
        todo
    WHERE
        id = #{id}
    AND
        finished = #{finished}
""")
fun select(@Param("id") id: Int, @Param("finished") finished: Boolean = false): Todo

といった感じで、@Paramを指定する必要がありました。仮に@Paramを指定しないと、バインド変数名はpram1param2という感じで引数の順番を使って機械的に割り当てられます。(順番とかイヤですよね・・・)

Kotlin 1.1からは?

Kotlin 1.1でコンパイルオプションとして-java-parameters(Java SE 8から追加された-parametersオプションと同等)を指定すると・・・

@Select("""
    SELECT
        id, title, details, finished
    FROM
        todo
    WHERE
        id = #{id}
    AND
        finished = #{finished}
""")
fun select(id: Int, finished: Boolean = false): Todo // @Paramが省略可能!!

といった感じで、@Paramの指定は省くことができるようになります:laughing:

Note:

これは、https://youtrack.jetbrains.com/issue/KT-8816 で対応されKotlin 1.1-M3でリリースされています。

実際にKotlin 1.1を使ってみよう!!

百聞は一見にしかず・・・ということで、実際にKotlin 1.1とMyBatis 3.4.1+を使って確認しましょう。本投稿では、以前投稿した「Kotlinでmybatis-spring-boot-starterを使う」をベースに説明していきます。

動作検証バージョン

  • Kotlin 1.1
  • MyBatis Spring Boot 1.2.0
  • MyBatis 3.4.2
  • Spring Boot 1.5.1.RELEASE
  • Maven 3.3.9 (ビルドツール)
  • IntelliJ IDEA 2016.3.4 (IDE)
  • Mac (OS)

ソースを直そう

まず、TodoMapperを以下のように直します。Mapperメソッドの引数を複数にするのがポイントです(引数がひとつだとバインド変数名に特に制約はなく、何を指定しても動くので)。

src/main/kotlin/com/example/mapper/TodoMapper.kt
@Mapper
interface TodoMapper {

    @Insert("""
        INSERT INTO todo
            (title, details, finished)
        VALUES
            (#{title}, #{details}, #{finished})
    """)
    @Options(useGeneratedKeys = true)
    fun insert(todo: Todo)

    @Select("""
        SELECT
            id, title, details, finished
        FROM
            todo
        WHERE
            id = #{id}
+        AND
+            finished = #{finished}
    """)
-    fun select(id: Int): Todo
+    fun select(id: Int, finished: Boolean = false): Todo

}

次に、Mapperメソッドの呼び出し元も直しましょう。

src/main/kotlin/com/example/MybatisKotlinDemoApplication.kt
@SpringBootApplication
open class MybatisKotlinDemoApplication : CommandLineRunner {

    @Autowired
    lateinit var todoMapper: TodoMapper

    @Transactional
    override fun run(vararg args: String?) {
        val newTodo: Todo = Todo()
        newTodo.title = "飲み会"
        newTodo.details = "銀座 19:00"

        todoMapper.insert(newTodo) // 新しいTodoをインサートする

-        val loadedTodo: Todo = todoMapper.select(newTodo.id)
+        val loadedTodo: Todo = todoMapper.select(id = newTodo.id) // idのみ指定、finishedはデフォ値のfalseが使われる
        println("ID       : " + loadedTodo.id)
        println("TITLE    : " + loadedTodo.title)
        println("DETAILS  : " + loadedTodo.details)
        println("FINISHED : " + loadedTodo.finished)
    }

}

fun main(args: Array<String>) {
    SpringApplication.run(MybatisKotlinDemoApplication::class.java, *args)
}

試しに・・・この状態で実行するとどうなるかMavenコマンドを使って試してみましょう。

$ ./mvnw clean spring-boot:run

以下のようなスタックトーレスが表示され、idという名前のバインド変数がみつからないよ〜というエラーがでてしまいます。

...
        ... 12 more
Caused by: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [0, 1, param1, param2]
        at org.apache.ibatis.binding.MapperMethod$ParamMap.get(MapperMethod.java:186)
...

Mavenコマンド上での実行

まず、POMファイルを以下のように直しましょう。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>mybatis-kotlin-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>mybatis-kotlin-demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
-        <version>1.4.1.RELEASE</version>
+        <version>1.5.1.RELEASE</version> <!-- 投稿更新時点の最新を使うように修正(修正は任意) -->
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
-        <kotlin.version>1.0.3</kotlin.version>
+        <kotlin.version>1.1.0</kotlin.version> <!-- 1.1の最新を使うように修正 -->
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
            <version>${kotlin.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
-            <version>1.1.1</version>
+            <version>1.2.0</version> <!-- 1.2.0にすることでMyBatis 3.4.2が推移的に依存アーティファクトに追加される -->
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

    </dependencies>

    <build>
        <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
        <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <artifactId>kotlin-maven-plugin</artifactId>
                <groupId>org.jetbrains.kotlin</groupId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>test-compile</goal>
                        </goals>
                    </execution>
                </executions>
+                <configuration>
+                    <args>
+                        <arg>-java-parameters</arg> <!-- Kotlin 1.1でサポートされた -java-parametersオプションを指定 -->
+                    </args>
+                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

POMファイルを修正後に再度Mavenコマンドを実行すると・・・

$ ./mvnw clean spring-boot:run

正常に動作するようになりました :laughing:

...
2017-03-02 22:00:55.855  INFO 85743 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
ID       : 1
TITLE    : 飲み会
DETAILS  : 銀座 19:00
FINISHED : false
2017-03-02 22:00:55.932  INFO 85743 --- [           main] c.e.MybatisKotlinDemoApplicationKt       : Started MybatisKotlinDemoApplicationKt in 1.84 seconds (JVM running for 9.21)
...

Note:

ちなみに・・・-java-parametersオプションを指定しないでコンパイルを行うと・・・引数の名前がarg0arg1といった感じになり、その値がバインド変数名として使われるので、以下のようなエラーが発生してしまいます。

...
       ... 12 more
Caused by: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
       at org.apache.ibatis.binding.MapperMethod$ParamMap.get(MapperMethod.java:195)
...

IntelliJ IDEA上での実行

Mavenコマンドを使って動作することは確認できましたが・・・開発時はIDEの機能を使って実行するのが一般的です。本投稿ではKotlinの開発元でもあるJetBrains社提供のIntelliJ IDEA上でKotlin 1.1を扱えるように設定し、修正したプログラムを実行します。(Eclipse Pluginも出ているみたいですが・・・今回は扱いません)

まず・・・Kotlin 1.1サポートのPluginに更新しましょう。
「ウィンドウメニューのTools → Kotlin → Configure Kotlin Plugin Updates」を選択後、「Stable」を選んで「Check for updates now」ボタンをクリックし、更新対象があれば「Install」ボタンをクリックしてプラグインを更新してください。

次に、プロジェクトで使うKotlinの言語バージョンやコンパイルオプションを設定を行います。
「ウィンドウメニューのPreferences → Build, Execution, Deployment → Compiler → Kotlin Compiler 」を選択後、

  • 「Language version」と「API version」を「1.1」に変更
  • 「Additional Command Line Parameters」に「-java-parameters」を追加

して「OK」ボタンを押下します。

kotlin11-compiler-settings.png

設定を変更した後は、必ず「ウィンドウメニューのBuild → Rebuild Project」を選択して、変更した設定でビルド(コンパイル)し直してください。(これ忘れると期待通り動いてくれない可能性があります・・・)

最後に・・・IDE上でMybatisKotlinDemoApplicationを選んでプログラムを実行すると、正常に動作するようになります :laughing:

...
2017-03-02 22:37:28.315  INFO 86040 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
ID       : 1
TITLE    : 飲み会
DETAILS  : 銀座 19:00
FINISHED : false
2017-03-02 22:37:28.390  INFO 86040 --- [           main] c.e.MybatisKotlinDemoApplicationKt       : Started MybatisKotlinDemoApplicationKt in 2.282 seconds (JVM running for 2.677)
...

まとめ

全面的にJavaからKotlinに移行できなくても、Mapperインタフェースだけ複数行文字列をサポートしているKotlinを使うという選択肢は、個人的にはかなり「アリ」だと思っています。Kotlin 1.0だと実際の引数名がバインド変数名として扱えないところが残念だな〜と思っていたのですが・・・Kotlin 1.1からバッチリ扱えるようになるので、MyBatisをアノテーション駆動(SQLをアノテーションで指定)で使っているユーザは試してみる価値があると思います。

なお・・・JavaとKotlinを共存させる場合のkotlin-maven-pluginを使ったコンパイル方法については・・・、Kotlinのリファレンスページをご覧ください。