Spring Boot キャンプ : Spring Boot + MyBatis編

Spring Bootキャンプシリーズ、Spring Boot + MyBatis編です。

## 今回の目的

Spring BootアプリでMyBatisを利用してDBアクセスを行います。

## 今回使用するライブラリ

* spring-boot-starter:2.2.0.M4
* mybatis-spring-boot-starter:2.0.1
* mybatis-spring-boot-starter-test:2.0.1
* h2:1.4.199
* lombok:1.18.8

以降の手順のいくつかはSpring Initializrでプロジェクトを作成することにより解決されます。

## DBアクセス

MyBatisの実装を行う前に、まずはDBアクセスするための設定を行います。

### DBアクセスの定義

Spring BootではDBにアクセスする`DataSource`等のBean定義を自動的に行ってくれます。
デフォルトではインメモリのH2データベースにアクセスするため、依存関係にh2を追加する必要があります。

**pom.xml**

```xml
    <dependencies>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
```

> Note.
> Spring Bootの設定ファイルでドライバと接続先を変更すれば、別DBにアクセスできます。（依存関係に対応するドライバを追加する必要があります）
> 
> **src/main/resources/application.yml**
> 
> ```yaml
> spring:
>   datasource:
>     driver-class-name: org.postgresql.Driver
>     url: jdbc:postgresql://localhost:5432/testdb
>     username: postgres
>     password: postgres
> ```

src/main/resources直下に以下のSQLファイルを作成すると、アプリ起動時にデータベースの初期化も自動的に行うこともできます。
デフォルトで認識されるSQLファイルは以下の通りです。

* schema.sql
* schema-${platform}.sql
* data.sql
* data-${platform}.sql

見たままですが、schema.sqlにはDDLを定義し、data.sqlにはDMLを定義します。

> Note.
> `${platform}`は`spring.datasource.platform`プロパティで指定します。
> 単体テスト時はH2、結合テスト時はPostgresqlといった使い分けができそうですね。

今回はschema.sqlを作成してテーブルを作成します。

**src/main/resources/schema.sql**

```sql
create table if not exists todo (
    todo_id identity,
    todo_title varchar(30),
    finished boolean,
    created_at timestamp
);
```

## mybatis-spring-boot-starter

Spring BootでMyBatisを利用するためのスターターです。
ぶっちゃけ詳細は開発者の[@kazuki43zooさんの記事](https://qiita.com/kazuki43zoo/items/ea79e206d7c2e990e478)が詳しいので、そちらを見たほうが良いですw

Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでMyBatisを使用するためのBean定義を自動的に行ってくれます。開発者は依存関係にmybatis-spring-boot-starterを追加するだけでOKです。

**pom.xml**

```xml
    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.1</version>
        </dependency>
    </dependencies>
```

**src/main/java/*/Application.java**

```java
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

メインクラスはSpring Bootアプリのデフォルトから何も変更していません。

### Domainクラス

DBのデータをマッピングするドメインクラスは、普通のJava BeanでOKです。
以下では、GetterやSetterの実装を省略するため、Lombokの`@Data`を利用しています。

**src/main/java/*/domain/Todo.java**

```java
@Data
public class Todo {
	private String todoId;
	private String todoTitle;
	private boolean finished;
	private LocalDateTime createdAt;
}
```

### Repositoryインターフェイス

SpringのRepositoryインターフェイスには、MyBatisのMapperを対応させます。
MyBatisのMapperは以下のいずれかの方法で実装することができます。

* Repositoryインターフェイスに対応するMapper XMLを作成する
* Repositoryインターフェイスに`@Mapper`を付与する

使いたいほうを使ってください！w
今回はアノテーションベースSpring Bootに合わせてMyBatisのMapperも`@Mapper`で実装します。

**src/main/java/*/repository/TodoRepository.java**

```java
@Mapper // (1)
public interface TodoRepository {

    // (2)
    @Select("SELECT todo_id, todo_title, finished, created_at FROM todo WHERE todo_id = #{todoId}")
    Optional<Todo> findById(String todoId);

    @Select("SELECT todo_id, todo_title, finished, created_at FROM todo")
    Collection<Todo> findAll();

    @Insert("INSERT INTO todo (todo_title, finished, created_at) VALUES (#{todoTitle}, #{finished}, #{createdAt})")
    @Options(useGeneratedKeys = true, keyProperty = "todoId") // (3)
    void create(Todo todo);

    @Update("UPDATE todo SET todo_title = #{todoTitle}, finished = #{finished}, created_at = #{createdAt} WHERE todo_id = #{todoId}")
    boolean updateById(Todo todo);

    @Delete("DELETE FROM todo WHERE todo_id = #{todoId}")
    void deleteById(Todo todo);

    @Select("SELECT COUNT(*) FROM todo WHERE finished = #{finished}")
    long countByFinished(boolean finished);
}
```

(1)　Repositoryインターフェイスに`@Mapper`を付与すると、MyBatisが自動的にスキャンしてMapperに登録してくれます。Repositoryインターフェイスはメインクラス配下のパッケージに置きましょう。

(2)　メソッドに付与した`@Select`・`@Insert`・`@Update`・`@Delete`に、実行するSQLを実装します。SQL内の`#{}`で引数を利用していますが、同じファイル内に記載するのでXMLより分かりやすいですね。

(3)　`@Options`は通常と異なる設定でSQLを実行する必要がある場合に付与します。ここではテーブルのキー項目がIDENTITY列のためDB側で自動採番されますが、`@Options`を利用することで自動採番されたIDを利用することができます。

#### 複数行のSQL

上ではSQLを1行で記載していますが、可読性が悪いので複数行で記載することをお勧めします。
こんな感じで文字列結合する形になるので、XMLよりちょっと見にくいですね。

```java
    @Select("SELECT"
            + " todo_id,"
            + " todo_title,"
            + " finished,"
            + " created_at"
            + " FROM todo"
            + " WHERE"
            + " todo_id = #{todoId}")
    Optional<Todo> findById(String todoId);
```

#### Select結果の自動マッピングと手動マッピング

例示したSelect文の結果カラム名とTodoクラスのプロパティ名が異なるため、Select結果はマッピングに失敗します。（`todo_title`と`todoTitle`など）

これを解決するには以下のいずれかの方法があります。

* MyBatisのネーミングルールによる自動マッピング
* `@Results`および`@ResultMap`による手動マッピング

個人的には、自動マッピングが利用できるようにプロパティ名を命名し、ルールから逸脱する場合のみ手動マッピングすることをお勧めします。

##### 自動マッピング

アンダースコア区切りのカラム名とキャメルケースのプロパティ名が一致していれば、MyBatisのネーミングルールによる自動マッピングが可能です。

Spring Bootの設定ファイルに以下の設定を追加します。

**src/main/resources/application.yml**

```yaml
mybatis:
  configuration:
    map-underscore-to-camel-case: true
```

> Note.
> `mybatis.configuration.*`プロパティでMyBatisの設定を変更することができます。設定可能なプロパティは[MyBatis 3 - 設定](http://www.mybatis.org/mybatis-3/ja/configuration.html#settings)を見てください。

##### 手動マッピング

カラム名とプロパティ名が一致しない場合、SQLごとに`@Results`および`@ResultMap`による手動マッピングを定義する必要があります。

* `@Results`で手動マッピングルールを定義する
* `@Results`で定義したルールを流用したければ、`@ResultMap`で`@Results`のIDを指定する

```java
    @Select("SELECT todo_id, todo_title, finished, created_at FROM todo WHERE todo_id = #{todoId}")
    @Results(id = "todo", value = {
            @Result(column = "todo_id", property = "todoId"),
            @Result(column = "todo_title", property = "todoTitle"),
            @Result(column = "finished", property = "finished"),
            @Result(column = "created_at", property = "createdAt") })
    Optional<Todo> findById(String todoId);

    @Select("SELECT todo_id, todo_title, finished, created_at FROM todo")
    @ResultMap("todo")
    Collection<Todo> findAll();
```

#### 動的SQLと共通化

Mapper XMLで可能だった動的SQL（`<if>`）や共通化（`<sql>`）は、SqlProviderで実現します。

```java
    // (2)
    @SelectProvider(type = TodoSqlProvider.class, method = "find")
    Optional<Todo> findById(String todoId);

    @SelectProvider(type = TodoSqlProvider.class, method = "find")
    Collection<Todo> findAll();

    // (1)
    public class TodoSqlProvider {
        public String find(String todoId) {
            return new SQL() {{
                SELECT("todo_id", "todo_title", "finished", "created_at");
                FROM("todo");
                if (todoId != null) {
                    WHERE("todo_id = #{todoId}");
                }
            }}.toString();
        }
    }
```

(1)　SqlProviderクラスを実装して、メソッドで`new SQL()`を利用してSQLを組み立てます。例ではインスタンスイニシャライザ（`new SQL() {{ココ}}`）を利用していますが、もちろん普通にメソッドチェーンで書いても良いです。

> Note.
> WHERE句には引数を直接埋め込むこともできますが、必ず`#{}`を利用しましょう。
> `#{}`はSQLインジェクションを防止してくれます。

(2)　メソッドに`@Select`の代わりに`@SelectProvider`を付与して、実装したSqlProviderクラスとメソッドを指定します。

> Note.
> インターフェイスのdefaultメソッドはSqlProviderメソッドにできません。必ずクラスを作る必要があります。
> これはSqlProviderメソッドを利用する際、SqlProviderクラスのインスタンスが生成されるためです。

## mybatis-spring-boot-starter-test

Spring BootでMyBatisをテストするためのスターターです。

Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでMyBatisのMapperをテストするためのBean定義を自動的に行ってくれます。開発者は依存関係にmybatis-spring-boot-starter-testを追加して、少しの設定をするだけでOKです。

**pom.xml**

```xml
    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>2.0.1</version>
        </dependency>
    </dependencies>
```

### JUnitテストケース

JUnitテストケースでは、クラスに`@MyBatisTest`を付与してテスト対象のリポジトリを`@Autowired`するだけでOKです。

**src/test/java/*/repository/TodoRepositoryTest.java**

```java
@MybatisTest
class TodoRepositoryTest {

    @Autowired
    TodoRepository todoRepository;

    @Test
    @Sql(statements = "INSERT INTO todo (todo_title, finished, created_at) VALUES ('sample todo', false, '2019-01-01')")
    void testFindAll() {
        // execute
        Collection<Todo> todos = todoRepository.findAll();

        // assert
        assertThat(todos)
                .hasSize(1)
                .extracting(Todo::getTodoTitle, Todo::isFinished, Todo::getCreatedAt)
                .containsExactly(tuple("sample todo", false, LocalDate.of(2019, 1, 1).atStartOfDay()));
    }
```

`@MyBatisTest`はMyBatisを利用するためのBean定義、DBにアクセスする`DataSource`等のBean定義を自動的に行ってくれますが、さらに**`@Transactional`を付与してくれます。**

`@Transactional`により、`@Sql`やテスト内で実行したSQLはテスト終了後にロールバックされます。インメモリではない実際のDBにアクセスしてテストするときもテストの独立性が保たれるので、安心ですね。

## まとめ

MyBatisのスターターを利用することで、設定周りを省略してMapperの実装に注力することができました。設定の変更もSpring Bootの設定ファイルにプロパティを定義するだけなので、非常に楽ですね。

MapperをXMLで実装する場合はもう少し設定する必要がありそうですが、それはまたの機会に。