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

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

## 今回の目的

Spring BootアプリでSpring Data JDBCのMyBatis連携機能を利用してDBアクセスを行います。

* Spring Data JDBCを利用したDBアクセスはこちら
* MyBatisを利用したDBアクセスはこちら

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

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

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

## DBアクセス

Spring Data JDBC + 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
);
```

## spring-boot-starter-data-jdbc + mybatis-spring-boot-starter

Spring BootでSpring Data JDBCとMyBatisを利用するためのスターターです。
Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでSpring Data JDBCとMyBatisを使用するためのBean定義を自動的に行ってくれます。

Spring Data JDBCとMyBatisを連携するには、依存関係にこれらを追加して、連携する`@Configuration`クラスを実装すればOKです。

**pom.xml**

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

**src/main/java/*/SpringDataJdbc.java**

```java
@Configuration
public class SpringDataJdbcConfig {
    @Bean
    @Primary
    public DataAccessStrategy mybatisDataAccessStrategy(SqlSession sqlSession) {
        return new MyBatisDataAccessStrategy(sqlSession);
    }
}
```

連携のため、Spring Data JDBCのDataAccessStrategyにMyBatisDataAccessStrategyクラスを利用します。
DataAccessStrategyはBean定義すれば、自動的に適用されます。

> Note.
> 自動的にDefalutDataAccessStrategyクラスのBeanが生成されるため、DataAccessStrategyのBean定義には`@Primary`を付与しないとBeanが重複してエラーとなります。

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

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

メインクラスはSpring Bootアプリのデフォルトから何も変更していません。
実装した`@Configuration`クラスはメインクラス配下のパッケージにあれば、自動的にスキャンされます。

### Spring Data JDBC + MyBatisでできること

CrudRepositoryのメソッドを実行したとき、Spring DataのNamedParameterJdbcTemplateではなくMyBatisを利用してSQLを発行することができます。

CrudRepositoryから連携されるMyBatisのMapperにはネーミングルールがあります。

```text
[DomainクラスのFQCN]Mapper.[予約メソッド名]
```

`com.example.domain.Todo`クラスなら、Mapper名は`com.example.domain.TodoMapper`のようになります。

予約メソッド名は例えば以下のようになりますが、詳細は[Spring Data JDBC - MyBatis Integration](https://docs.spring.io/spring-data/jdbc/docs/1.1.0.RC1/reference/html/#jdbc.mybatis)を確認してください。

* Mapperの`findById` -> CrudRepositoryの`findById`メソッドで使用される
* Mapperの`insert` -> CrudRepositoryの`save`や`saveAll`メソッドで使用される

ここで注意すべきことは、**MyBatisで自由にカスタムクエリを作れない**ということです。
カスタムクエリを作りたければ、Spring Data JDBCで`@Query`を利用するか、大人しくMyBatisを直接使いましょう。

また、CrudRepositoryメソッドの引数やMapperの戻り値は`MyBatisContext`で連携されます。
このため、MyBatisのみを使う場合とは**Mapperの実装方法が微妙に異なります。**

### Domainクラス

DBのデータをマッピングするドメインクラスには、プライマリキーを識別する`@ID`を付与します。
以下では、GetterやSetterの実装を省略するため、Lombokの`@Data`を利用しています。

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

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

> Note.
> テーブル名とDomainクラス名が異なるときは、クラスに`@Table("テーブル名")`を付与すればOKです。

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

SpringのRepositoryインターフェイスは、Spring DataのCrudRepositoryインターフェイスを継承します。

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

```java
// (1)
public interface TodoRepository extends CrudRepository<Todo, String> {

   // (2)
}
```

(1)　RepositoryインターフェイスはCrudRepositoryを継承します。ジェネリクスは`<Domainクラスの型, IDの型>`です。

(2)　CrudRepositoryを継承すると、自動的にCRUDをサポートするデフォルトメソッド（クエリ）が利用可能になります。提供されるメソッドは[CrudRepositoryのJavaDoc](https://docs.spring.io/spring-data/commons/docs/2.2.0.M4/api/org/springframework/data/repository/CrudRepository.html)を確認すると分かります。

### MyBatisのMapperインターフェイス

MyBatisのMapperインターフェイスは、Domainクラスと同じパッケージに実装します。
MapperはXMLと`@Mapper`を付与したインターフェイスのいずれかで実装できますが、ここではインターフェイスで実装することにします。

**src/main/java/*/domain/TodoMapper.java**

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

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

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

    @Insert("INSERT INTO todo (todo_title, finished, created_at) VALUES (#{instance.todoTitle}, #{instance.finished}, #{instance.createdAt})")
    @Options(useGeneratedKeys = true, keyProperty = "instance.todoId")
    Todo insert(MyBatisContext context);

    @Update("UPDATE todo SET todo_title = #{instance.todoTitle}, finished = #{instance.finished}, created_at = #{instance.createdAt} WHERE todo_id = #{id}")
    Todo update(MyBatisContext context);

    @Delete("DELETE FROM todo WHERE todo_id = #{id}")
    void delete(MyBatisContext context);

    @Select("SELECT COUNT(*) FROM todo WHERE finished = 'FALSE'")
    long count(MyBatisContext context);
}
```

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

(2)　メソッドに付与した`@Select`・`@Insert`・`@Update`・`@Delete`に、実行するSQLを実装します。SQL内の`#{}`で引数を利用していますが、以下のルールに従う必要があります。

* Domainクラスの`@ID`項目 -> `#{id}`
* Domainクラスの`@ID`以外の項目 -> `#{instance.[プロパティ名]}`

(3) メソッドシグネチャには以下のルールがあります。

* メソッド名は先述の予約メソッド名にする必要があります。メソッド名が異なると呼び出されません。
* メソッド引数は必ず`MyBatisContext`にする必要があります。
* メソッド戻り値はCrudRepositoryメソッドと合わせる必要がありますが、いくつか異なります。
  - `Optional`はあってもなくてもOKでした。
  - `Iterable`は対応しておらず、`Collection`等にする必要がありました。

> Note.
> ここではテーブルのカラム名とDomainクラスのプロパティ名の違いを吸収するため、Spring Bootの設定ファイルに`mybatis.configuration.map-underscore-to-camel-case`プロパティを指定しています。

#### DomainクラスとMapperインターフェイスのパッケージを分離

前述ではDomainクラスとMapperインターフェイスを同パッケージに置きましたが、数が増えると整理が面倒なので、整理のためにパッケージを分離します。

CrudRepositoryと連携するMapperのネーミングルールはNamespeceStrategyインターフェイスを実装することで変更することが可能です。
が、、、NamespeceStrategyで変更可能なのは`[DomainクラスのFQCN]Mapper`の部分だけで、予約メソッド名を変更するにはMyBatisDataAccessStrategyを拡張する必要があります。

ここでは、NamespeceStrategyを実装してネーミングルールを以下のように変更します。

```text
[Domainクラスのパッケージ名].mapper.[Domainクラス名]Mapper.[予約メソッド名]
```

**src/main/java/*/SpringDataJdbc.config**

```java
@Configuration
public class SpringDataJdbcConfig {
    @Bean
    @Primary
    public DataAccessStrategy mybatisDataAccessStrategy(SqlSession sqlSession) {
        MyBatisDataAccessStrategy strategy = new MyBatisDataAccessStrategy(sqlSession);
        strategy.setNamespaceStrategy(new NamespaceStrategy() {
            @Override
            public String getNamespace(Class<?> domainType) {
                return domainType.getPackage().getName() + ".mapper." + domainType.getSimpleName() + "Mapper";
            }
        });
        return strategy;
    }
}
```

NamespaceStrategyインターフェイスを実装して`getNamespace`メソッドでMapper名を決め、MyBatisDataAccessStrategyにセットします。

あとは、Mapperインターフェイスを`mapper`パッケージに移動すればOKです。

## spring-boot-starter-test + mybatis-spring-boot-starter-test

Spring BootとMyBatisをテストするためのスターターです。
Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでSpring Data JDBCとMyBatisを使用してテストするためのBean定義を自動的に行ってくれます。

Spring Data JDBCとMyBatisを連携したテストをするには、依存関係にこれらを追加して、少しの設定をするだけでOKです。

**pom.xml**

```xml
    <dependencies>
        <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-test</artifactId>
            <version>2.0.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
```

> Note.
> 厳密にはmybatis-spring-boot-starter-testではなくmybatis-spring-boot-starterのみでもOKなんですが、ここではmainの文法とtestの文法を明確に分けるため使用します。

### JUnitテストケース

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

* `@DataJdbcTest` -> Spring Data JDBCを有効化します。
* `@AutoConfigureMybatis` -> MyBatisを有効化します。
* `@Import` -> MyBatisDataAccessStrategyを有効化する設定を読み込みます。

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

```java
@DataJdbcTest
@AutoConfigureMybatis // (1)
@Import(SpringDataJdbcConfig.class) // (2)
@Transactional // (3)
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
        Iterable<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()));
    }
```

(1)　`@DataJdbcTest`と`@MyBatisTest`を同時に使うことはできないので、`@DataJdbcTest`に`@AutoConfigureMybatis`を追加する形で使用します。

(2)　`@DataJdbcTest`等のテスト用Auto-Configは、軽量化のため限定的にBeanを読み込むため、独自の`@Configuration`クラス等は読み込まれません。アプリ実行時とテスト時で異なる設定をする必要があるため、開発者はある程度Auto-Configを理解してないとダメですね。

(3)　`@DataJdbcTest`は`@Transactional`を付与してくれないので、テスト終了後にロールバックするためには**自分で`@Transactional`を付与する**必要があります。

> Note.
> Spring Boot 2.2.2から`@DataJdbcTest`で`@Transactional`が付与されるようになりました。

## まとめ

Spring Data JDBCからMyBatisの連携も、ほとんど設定なしに実現することができました。
ただし、MyBatisの利用には一定の制限がつくことになるので、連携して何が嬉しいかと言われると、、、

DomainクラスへのCRUDに限定してMyBatisをシンプルに利用することを目的とするような設計思想であったり、DBの複雑なテーブルをまとめてViewとしてCRUDアクセスするような限定的なケースで利用するのかな、と思いました。