MyBatis2(iBatis)をSpring Boot 1.4(Spring 4)でつかう方法

今回は今更ながら・・・・Spring Boot 1.4(Spring 4.3)上でMyBatis 2(iBatisといった方が馴染みがあるかな？）を使う方法を紹介します。なぜ今頃MyBatis 2の話やねん？と思う方がほとんどだと思います:sweat_smile:が、このエントリーを書こうと思った動機は・・・Spring 3系がついに今年の12月（あと半月・・・）で[EOL](https://spring.io/blog/2015/06/15/spring-framework-3-2-x-eol-on-dec-31-2016)になるためです。

# MyBatis 2とSpringの関係
Springは3系まではMyBatis 2との連携コンポーネントを提供していましたが、Spring 4系から華麗にDropされています。そのため、MyBatis 2を使っているアプリケーション（レガシーなアーキテクチャで作られているアプリ）はSpring 4にバージョンアップすることができませんでした。これまでは脆弱性など重要なバグが見つかればSpring 3系にもフィードバックされていましたが、**来年からはフィードバックされません。セキュリティ脆弱性などのことを考えるとSpring 4系（or 来年4月頃にリリースされる予定のSpring 5系）へのバージョンアップを検討すべきです！！**

> **Note:**
>
> ちなみに・・・MyBatisの最新バージョンは3.4.1ですが、MyBatis 2とは互換性が全くありません。また、MyBatis 3とSpringの連携コンポーネントは、SpringではなくMyBatisの開発コミュニティーから提供されています。

# で、どうすればいいの？

たぶんほとんど知られていないと思いますが・・・MyBatisの開発コミュニティーから「[ibatis-spring](https://github.com/mybatis/ibatis-spring)」なるものが提供されているんです:grin:
こいつの実体は、Spring 3系で提供されているMyBatis 2との連携コンポーネントの完全クローンなので、jarをぶち込むだけでSpring 4上でMyBatis 2を使うことができるのです。

# Spring Boot 1.4上でMyBatis 2を使ってみよう！

論より証拠・・・ということで、Spring Boot 1.4(Spring 4.3)上でMyBatis 2が動くか確認してみることにします。

まずは、開発プロジェクトを「[SPRING INITIALIZR](https://start.spring.io/)」を使って作成します。依存アーティファクトには、「JDBC」と「H2」を指定します。

<img width="1327" alt="spring-initializr.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/cbca4c50-8853-8cb5-478f-c03668917cfa.png">

完成品は以下のGitHubリポジトリで公開してます。

* https://github.com/kazuki43zoo/mybatis2-demo


> **Note:**
> 2016/12/17 13:30 追記
>
> Spring Boot 2.0(Spring 5.0)のSNAPSHOTバージョンでの動作確認も行いました！！結果はもちろん:thumbsup:


# mybatis2とibatis-springを依存アーティファクトに追加


```xml:pom.xml
<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis2</artifactId>
	<version>2.3.6</version>
</dependency>

<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis-2-spring</artifactId>
	<version>1.0.3</version>
</dependency>
```

# todoテーブルの作成

H2の組み込みデータベースにtodoテーブルを作成します。
Spring Bootの自動コンフィギュレーションで作成される`DataSource`を使う場合は、クラスパス直下に`schema.sql`と`data.sql`というファイルを配置しておくと、Spring Boot起動時にこれらのファイルを読み込んでSQLを実行してくれます。

```sql:src/main/resources/schema.sql
CREATE TABLE todo (
    id INTEGER
    ,title TEXT NOT NULL
    ,details TEXT
    ,finished BOOLEAN NOT NULL
);

CREATE SEQUENCE seq_todo;
```

> **Note:**
>
> 本当はidカラムはID列にして、JDBC 3.0から追加された`Statement#getGeneratedKeys()`を使いたいところだけど・・・MyBatis 2はどうやらサポートしてなさそう。（MyBais 3は連携できます）
> なので・・・ここでは昔ながらにシーケンスを使うスタイルになっております（あしからず）


# ドメインオブジェクトの作成

今回は、ドメインオブジェクトとして`Todo`クラスを作ります。

```java:src/main/java/com/example/domain/Todo.java
package com.example.domain;

public class Todo {
    private int id;
    private String title;
    private String details;
    private boolean finished;
    // ... setter and getter
}
```

# DAOクラスの作成

今回は、`Todo`へのCRUD操作を提供するDAOクラスを作ります。

```java:src/main/java/com/example/dao/TodoDao.java
package com.example.dao;

import com.example.domain.Todo;
import org.springframework.orm.ibatis.SqlMapClientOperations;
import org.springframework.stereotype.Repository;

@Repository
@SuppressWarnings("deprecation") // 非推奨の警告を削除・・・（知っておりますw）
public class TodoDao {

    private final SqlMapClientOperations operations;

    public TodoDao(SqlMapClientOperations operations) {
        this.operations = operations;
    }

    public void insert(Todo todo) {
        operations.insert("com.example.dao.TodoDao.insert", todo);
    }

    public Todo select(int id) {
        return (Todo) operations.queryForObject("com.example.dao.TodoDao.select", id);
    }

}
```

> **Note:**
>
> ibatis-springはSpring 3系の完全クローンなので、`@Deprecated`がついたままです。非推奨は１００も承知で使っているので、クラスレベルに`@SuppressWarnings("deprecation")`をつけて警告を削除しています。

# MyBatis 2の設定ファイルの作成

MyBatis 2の設定ファイルを作成し、MyBatis 2の動作を指定します。

```xml:src/main/resources/mybatis/sqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
		PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
		"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
	<settings useStatementNamespaces="true"/> <!-- ネームスペースを有効にする -->
</sqlMapConfig>
```

# SQLマッピングファイルの作成

SQLマッピングファイルを作成し、TodoオブジェクトのCRUD操作を行うためのSQLを記述します。

```xml:src/main/resources/mybatis/todo-sqlmap.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
		PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
		"http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="com.example.dao.TodoDao">

	<insert id="insert" parameterClass="com.example.domain.Todo">

		<selectKey keyProperty="id" resultClass="int" type="pre">
			SELECT seq_todo.nextval
		</selectKey>

		INSERT INTO todo
		(
			id
			,title
			,details
			,finished
		)
		values(
			#id#
			,#title#
			,#details#
			,#finished#
		)
	</insert>
	<select id="select" parameterClass="int" resultClass="com.example.domain.Todo">
		SELECT
			id, title, details, finished
		FROM
			todo
		WHERE
			id = #id#
	</select>

</sqlMap>
```

# MyBatis 2用のBean定義

`SqlMapClient`と`SqlMapClientTemplate`のBean定義を行います。 ここでは`Mybatis2DemoApplication`のインナークラスとしてConfigクラスを作成しています。

```java
package com.example;

import com.ibatis.sqlmap.client.SqlMapClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.orm.ibatis.SqlMapClientFactoryBean;
import org.springframework.orm.ibatis.SqlMapClientOperations;
import org.springframework.orm.ibatis.SqlMapClientTemplate;

import javax.sql.DataSource;
import java.io.IOException;

@SpringBootApplication
public class Mybatis2DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(Mybatis2DemoApplication.class, args);
    }

    // MyBatis 2用のBean定義
    @Configuration
    @SuppressWarnings("deprecation")
    static class MyBatisConfig {

        private final ResourcePatternResolver resourcePatternResolver;

        MyBatisConfig(ResourcePatternResolver resourcePatternResolver) {
            this.resourcePatternResolver = resourcePatternResolver;
        }

        @Bean
        SqlMapClientFactoryBean sqlMapClient(DataSource dataSource) throws IOException {
            SqlMapClientFactoryBean factoryBean = new SqlMapClientFactoryBean();
            factoryBean.setConfigLocations(resourcePatternResolver.getResources("classpath*:/mybatis/sqlMapConfig.xml"));
            factoryBean.setMappingLocations(resourcePatternResolver.getResources("classpath*:/mybatis/**/*-sqlmap.xml"));
            factoryBean.setDataSource(dataSource);
            return factoryBean;
        }

        @Bean
        SqlMapClientOperations sqlMapClientOperations(SqlMapClient sqlMapClient) {
            return new SqlMapClientTemplate(sqlMapClient);
        }

    }

}
```

# Mybatis2DemoApplicationの修正とSpring Bootアプリケーションの起動

`Mybatis2DemoApplication`を修正し、DAOクラス(ibatis-spring)を経由してデータベースにアクセスします。

```java
package com.example;

import com.example.dao.TodoDao;
import com.example.domain.Todo;
import com.ibatis.sqlmap.client.SqlMapClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.orm.ibatis.SqlMapClientFactoryBean;
import org.springframework.orm.ibatis.SqlMapClientOperations;
import org.springframework.orm.ibatis.SqlMapClientTemplate;
import org.springframework.transaction.annotation.Transactional;

import javax.sql.DataSource;
import java.io.IOException;

@SpringBootApplication
public class Mybatis2DemoApplication implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(Mybatis2DemoApplication.class, args);
    }

    private final TodoDao todoDao;

    Mybatis2DemoApplication(TodoDao todoDao) {
        this.todoDao = todoDao;
    }

    @Transactional
    @Override
    public void run(String... args) throws Exception {
        Todo newTodo = new Todo();
        newTodo.setTitle("飲み会");
        newTodo.setDetails("銀座 19:00");

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

        Todo loadedTodo = todoDao.select(newTodo.getId()); // インサートしたTodoを取得して標準出力する
        System.out.println("ID       : " + loadedTodo.getId());
        System.out.println("TITLE    : " + loadedTodo.getTitle());
        System.out.println("DETAILS  : " + loadedTodo.getDetails());
        System.out.println("FINISHED : " + loadedTodo.isFinished());
    }


    @Configuration
    @SuppressWarnings("deprecation")
    static class MyBatisConfig {

        private final ResourcePatternResolver resourcePatternResolver;

        MyBatisConfig(ResourcePatternResolver resourcePatternResolver) {
            this.resourcePatternResolver = resourcePatternResolver;
        }

        @Bean
        SqlMapClientFactoryBean sqlMapClient(DataSource dataSource) throws IOException {
            SqlMapClientFactoryBean factoryBean = new SqlMapClientFactoryBean();
            factoryBean.setConfigLocations(resourcePatternResolver.getResources("classpath*:/mybatis/sqlMapConfig.xml"));
            factoryBean.setMappingLocations(resourcePatternResolver.getResources("classpath*:/mybatis/**/*-sqlmap.xml"));
            factoryBean.setDataSource(dataSource);
            return factoryBean;
        }

        @Bean
        SqlMapClientOperations sqlMapClientOperations(SqlMapClient sqlMapClient) {
            return new SqlMapClientTemplate(sqlMapClient);
        }

    }

}
```

`MybatisDemo2Application`を修正したら、Spring Bootアプリケーションとして起動します。

```text:コンソール
$ ./mvnw spring-boot:run
...
2016-12-17 10:37:21.407  INFO 10597 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
ID       : 1
TITLE    : 飲み会
DETAILS  : 銀座 19:00
FINISHED : false
2016-12-17 10:37:21.501  INFO 10597 --- [           main] com.example.Mybatis2DemoApplication      : Started Mybatis2DemoApplication in 1.463 seconds (JVM running for 4.619)
...
```

標準出力にインサートしたTodoの状態が出力されました :clap: :clap: :clap:

ちなみに、開発プロジェクトは以下のような状態になります。

```text:ディレクトリ構成
.
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           ├── Mybatis2DemoApplication.java // 修正したファイル
    │   │           ├── dao
    │   │           │   └── TodoDao.java // 追加したファイル
    │   │           └── domain
    │   │               └── Todo.java // 追加したファイル
    │   └── resources
    │       ├── application.properties
    │       ├── mybatis
    │       │   ├── sqlMapConfig.xml // 追加したファイル
    │       │   └── todo-sqlmap.xml  // 追加したファイル
    │       └── schema.sql // 追加したファイル
    └── test
        └── java
            └── com
                └── example
                    └── Mybatis2DemoApplicationTests.java
```

# まとめ

とりあえず、問題なく動くことが確認できました！！
個人的にはSpring 4上でMyBatisを使いたいならMyBatis 3を使えや〜と思っておりますが、大人の事情でそうもいってられないこともあると思います。とりあえず影響範囲をできるだけ最小限におさえてEOL対策したいと思うのも理解はできるので、「Spring 3系 + MyBatis 2を使っているアプリケーション」でSpring 3系のEOL対策をお考えの方は、Spring 4.3へのバージョンアップ + 「ibatis-spring」の利用を検討してみるとよいかと思います。

なお、Spring 3系からSpring 4系（or Spring 5系）への移行を考えている方は、以下のサイト（Spring本家の移行ガイド、TERASOLUNAの移行ガイド）が参考になると思います。

* https://github.com/spring-projects/spring-framework/wiki#migrating-from-earlier-versions
* https://github.com/terasolunaorg/terasoluna-gfw/wiki