spring-boot-starter-data-jdbc(2.1.0.BUILD-SNAPSHOT)を試す

昨日(2018/9/21)にSpring Data JDBC 1.0.0が正式にリリースされており、Spring BootでのサポートはSpring Boot 2.1からになります。すでに対応は完了している(2.1.0.M4でリリース予定の)ようなので、本エントリーではSpring Boot 2.1.0.BUILD-SNAPSHOTをさっと試してみたいと思います。

> **NOTE:**
>
> Spring Data JDBC本体については、「[Spring Data JDBC 1.0.0.BUILD-SNAPSHOT(-> 1.0.0.RELEASE)を試してみた
](https://qiita.com/kazuki43zoo/items/bd63d28dc2348aa21719)」をみてみてください。

# 検証バージョン

* Spring Data JDBC 1.0.0.RELEASE
* Spring Boot Starter Data JDBC 2.1.0.BUILD-SNAPSHOT (2018/09/22時点)


# 検証プロジェクト

[SPRING INITIALIZR](https://start.spring.io/)にて、

* Spring Bootのバージョンを「2.1.0(SNAPSHOT)」に選択
* Dependenciesに「H2」「JDBC」を追加

してダウンロード・解凍します（必要に応じてIDEにインポートします）。

残念ながら・・本エントリー執筆時点ではspring-boot-starter-data-jdbcへの依存関係は解決されないため、spring-boot-starter-jdbcへの依存をspring-boot-starter-data-jdbcに修正します（追加でもOKです）。

```xml:pom.xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
```

こうすると・・・spring-data-jdbcやspring-jdbcが依存アーティファクトに追加されAutoConfigure対象になります。

* https://github.com/kazuki43zoo/spring-data-jdbc-boot-demo

# Spring Data JDBCのAutoConfigure

まずは、Spring Data JDBCのAutoConfigureを使って（＝設定レスで）簡単なプログラムを動かしてみます。

### テーブル

テーブルを作ります。

```sql:src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS todo (
    id IDENTITY
    ,title VARCHAR NOT NULL
    ,details VARCHAR
    ,finished BOOLEAN NOT NULL
);
```

### ドメインオブジェクト

ドメインオブジェクトを作ります。

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

import org.springframework.data.annotation.Id;

public class Todo {
  @Id
  private int id;
  private String title;
  private String details;
  private boolean finished;

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getDetails() {
    return details;
  }

  public void setDetails(String details) {
    this.details = details;
  }

  public boolean isFinished() {
    return finished;
  }

  public void setFinished(boolean finished) {
    this.finished = finished;
  }

}
```

### リポジトリ

ドメインオブジェクトのCRUD操作を提供するリポジトリを作ります。

```java:src/main/java/com/example/demo/repository/TodoRepository.java
package com.example.demo.repository;

import org.springframework.data.repository.CrudRepository;

import com.example.demo.domain.Todo;

public interface TodoRepository extends CrudRepository<Todo, Integer> {

}
```

### デモアプリ

リポジトリのメソッドを使用して、ドメインオブジェクトを保存し、保存したドメインオブジェクトを参照する処理を実装します。

```java:src/main/java/com/example/demo/SpringDataJdbcBootDemoApplication.java
package com.example.demo;

import com.example.demo.domain.Todo;
import com.example.demo.repository.TodoRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.util.Optional;

@SpringBootApplication
public class SpringDataJdbcBootDemoApplication {

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

  @Bean
  CommandLineRunner demo(TodoRepository todoRepository) { // リポジトリをインジェクション
    return args -> {
      // ドメインオブジェクトを保存
      Todo newTodo = new Todo();
      newTodo.setTitle("飲み会");
      newTodo.setDetails("銀座 19:00");
      todoRepository.save(newTodo);

      // ドメインオブジェクトを参照
      Optional<Todo> todo = todoRepository.findById(newTodo.getId());
      System.out.println("ID       : " + todo.get().getId());
      System.out.println("TITLE    : " + todo.get().getTitle());
      System.out.println("DETAILS  : " + todo.get().getDetails());
      System.out.println("FINISHED : " + todo.get().isFinished());
    };

  }

}
```

デモアプリを起動すると、以下のような標準出力が出ます。


```console
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v2.1.0.BUILD-SNAPSHOT)

2018-09-22 13:06:12.787  INFO 4199 --- [           main] c.e.d.SpringDataJdbcBootDemoApplication  : Starting SpringDataJdbcBootDemoApplication on xxx with PID 4199 (/Users/xxx/Downloads/spring-data-jdbc-boot-demo/target/classes started by xxx in /Users/xxx/Downloads/spring-data-jdbc-boot-demo)
2018-09-22 13:06:12.792  INFO 4199 --- [           main] c.e.d.SpringDataJdbcBootDemoApplication  : No active profile set, falling back to default profiles: default
2018-09-22 13:06:13.240  INFO 4199 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2018-09-22 13:06:13.287  INFO 4199 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 45ms. Found 1 repository interfaces.
2018-09-22 13:06:13.585  INFO 4199 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2018-09-22 13:06:13.751  INFO 4199 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2018-09-22 13:06:13.959  INFO 4199 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2018-09-22 13:06:14.009  INFO 4199 --- [           main] c.e.d.SpringDataJdbcBootDemoApplication  : Started SpringDataJdbcBootDemoApplication in 1.513 seconds (JVM running for 2.113)
ID       : 1
TITLE    : 飲み会
DETAILS  : 銀座 19:00
FINISHED : false
2018-09-22 13:06:14.083  INFO 4199 --- [       Thread-5] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
2018-09-22 13:06:14.084  INFO 4199 --- [       Thread-5] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2018-09-22 13:06:14.085  INFO 4199 --- [       Thread-5] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
```

Spring Data JDBCとは直接関係ありませんが、デモアプリのテストも用意してみます。

```java:src/test/java/com/example/demo/SpringDataJdbcBootDemoApplicationTests.java
package com.example.demo;

import org.assertj.core.api.Assertions;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDataJdbcBootDemoApplicationTests {

  @ClassRule
  public static OutputCapture capture = new OutputCapture(); // 標準出力をキャプチャする

  @Test
  public void contextLoads() {
    Assertions.assertThat(capture.toString()).containsSubsequence(
        "ID       : 1",
        "TITLE    : 飲み会",
        "DETAILS  : 銀座 19:00",
        "FINISHED : false"
    );
  }

}
```

# リポジトリのテスト

予め用意されているメソッド（`CrudRepository`のメソッド）に対するテストを行う必要はないと思いますが、`@Query`やカスタムメソッドなどの仕組みを使って作成したクエリメソッドに対するテストは必要です。サービスクラスなどのテストと一緒にテストすることもあると思いますが、条件句などのバリエーションテストはリポジトリ単体でテストする方が効率的なことの方が多いでしょう。

そんな時は・・・`@DataJdbcTest`を使うと、リポジトリ単体でテストが簡単＋効率的に実施することができます。

```java:src/test/java/com/example/demo/repository/TodoRepositoryTests.java
package com.example.demo.repository;

import com.example.demo.domain.Todo;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Optional;

@RunWith(SpringRunner.class)
@DataJdbcTest // アノテーションを付与
public class TodoRepositoryTests {

  @Autowired
  private TodoRepository todoRepository; // テスト対象のリポジトリをインジェクション

  @Test
  public void saveAndFindById() {
    Todo newTodo = new Todo();
    newTodo.setTitle("飲み会");
    newTodo.setDetails("銀座 19:00");
    todoRepository.save(newTodo);

    Optional<Todo> todo = todoRepository.findById(newTodo.getId());
    Assertions.assertThat(todo.isPresent()).isTrue();
    Assertions.assertThat(todo.get().getId()).isEqualTo(newTodo.getId());
    Assertions.assertThat(todo.get().getTitle()).isEqualTo(newTodo.getTitle());
    Assertions.assertThat(todo.get().getDetails()).isEqualTo(newTodo.getDetails());
    Assertions.assertThat(todo.get().isFinished()).isFalse();
  }

}
```

テスト実行時のログを見ると・・・

* コネクションプールが使われていない
* 組み込みのインメモリデータベースが使われている

ことがわかります。
`@SpringBootTest`を付与したテストは、実際のアプリ実行時とほぼ同じ状態でテストが行われるため、リポジトリを動かすために必要ないコンポーネントも全てDIコンテナに登録されてしまいます（＝単体テストとしては非効率な環境）。

```console
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v2.1.0.BUILD-SNAPSHOT)

2018-09-22 13:22:14.786  INFO 4219 --- [           main] c.e.demo.repository.TodoRepositoryTests  : Starting TodoRepositoryTests on xxx with PID 4219 (started by xxx in /Users/xxx/Downloads/spring-data-jdbc-boot-demo)
2018-09-22 13:22:14.790  INFO 4219 --- [           main] c.e.demo.repository.TodoRepositoryTests  : No active profile set, falling back to default profiles: default
2018-09-22 13:22:15.117  INFO 4219 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2018-09-22 13:22:15.173  INFO 4219 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 52ms. Found 1 repository interfaces.
2018-09-22 13:22:15.219  INFO 4219 --- [           main] beddedDataSourceBeanFactoryPostProcessor : Replacing 'dataSource' DataSource bean with embedded version
2018-09-22 13:22:15.553  INFO 4219 --- [           main] o.s.j.d.e.EmbeddedDatabaseFactory        : Starting embedded database: url='jdbc:h2:mem:bb9acd52-6aa9-4d03-ae63-c4330675bef9;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
2018-09-22 13:22:16.178  INFO 4219 --- [           main] c.e.demo.repository.TodoRepositoryTests  : Started TodoRepositoryTests in 1.738 seconds (JVM running for 3.088)
ID       : 1
TITLE    : 飲み会
DETAILS  : 銀座 19:00
FINISHED : false
```

> **NOTE:**
>
> `@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)`を付与することで、組み込みのインメモリではなくアプリケーション実行時と同じ構成（コネクションプールや接続先など）でデータベースへアクセスすることもできます。

# AutoConfigureのカスタマイズ

現時点では・・・リポジトリのスキャン機能(=Spring Data JDBC)の有効・無効を制御するプロパティが用意されています。

```properties:src/main/resources/application.properties
# 機能を無効化したい場合は false を指定する （デフォルトは true: 有効）
spring.data.jdbc.repositories.enabled=false
```

# Spring Data JDBCのカスタマイズ

コンフィギュレーションプロパティでは、リポジトリのスキャン機能(=Spring Data JDBC)の有効・無効を制御することはできますが、Spring Data JDBCの動作をカスタマイズすることはできません。
Spring Data JDBC本体の動作をカスタマイズする場合は、`org.springframework.data.jdbc.repository.config.JdbcConfiguration`の継承クラスを作成し、作成したクラスをDIコンテナに登録（コンポーネントスキャンして登録）するのが良さそうです。
というのも・・・`JdbcConfiguration`に定義されているBeanをカスタマイズする場合は、`JdbcConfiguration`を継承したクラスで該当メソッドをオーバライドしないとSpring Bootを起動することができませんでした（少なくても・・・本エントリで紹介する`JdbcCustomConversions`をカスタマイズする場合は、クラス継承してメソッドをオーバライドするスタイルにしないとダメだった・・）。

> **NOTE:**
>
> コンポーネントによっては、DIコンテナに`@Bean`メソッドなどを利用してDIコンテナに登録しておくだけでSpring Data JDBCが自動検出してくれるものもあります。例えば・・・・
>
> * `DataAccessStrategy`
> * `NamingStrategy`
>
> などが該当します。
>
> また、リポジトリ関係の設定をカスタマイズしたい場合は、コンフィギュレーションクラスに`@EnableJdbcRepositories`を付与して属性値をカスタマイズすることができます。

本エントリでは、Spring Data JDBC(Spring JDBC)がサポート指定ないデータ型の`Converter`(`Clob` -> `String`)をSpring Data JDBCに追加する方法を紹介します。

H2でデータ型を`TEXT`にすると、

```sql
CREATE TABLE IF NOT EXISTS todo (
    id IDENTITY
    ,title TEXT NOT NULL -- TEXTへ変更
    ,details TEXT        -- TEXTへ変更
    ,finished BOOLEAN NOT NULL
);
```

デフォルトだと`Clob`を`String`に変換できずに以下のようなエラーが発生します。

```console
...
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.h2.jdbc.JdbcClob] to type [java.lang.String]
	at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:321) ~[spring-core-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:194) ~[spring-core-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:174) ~[spring-core-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.data.relational.core.conversion.BasicRelationalConverter.getPotentiallyConvertedSimpleRead(BasicRelationalConverter.java:230) ~[spring-data-jdbc-1.0.0.RELEASE.jar:1.0.0.RELEASE]
	at org.springframework.data.relational.core.conversion.BasicRelationalConverter.readValue(BasicRelationalConverter.java:160) ~[spring-data-jdbc-1.0.0.RELEASE.jar:1.0.0.RELEASE]
	at org.springframework.data.jdbc.core.EntityRowMapper.readFrom(EntityRowMapper.java:127) ~[spring-data-jdbc-1.0.0.RELEASE.jar:1.0.0.RELEASE]
	at org.springframework.data.jdbc.core.EntityRowMapper.populateProperties(EntityRowMapper.java:103) ~[spring-data-jdbc-1.0.0.RELEASE.jar:1.0.0.RELEASE]
	at org.springframework.data.jdbc.core.EntityRowMapper.mapRow(EntityRowMapper.java:74) ~[spring-data-jdbc-1.0.0.RELEASE.jar:1.0.0.RELEASE]
	at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:94) ~[spring-jdbc-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:61) ~[spring-jdbc-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:679) ~[spring-jdbc-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:617) ~[spring-jdbc-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:669) ~[spring-jdbc-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:694) ~[spring-jdbc-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:748) ~[spring-jdbc-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.queryForObject(NamedParameterJdbcTemplate.java:232) ~[spring-jdbc-5.1.0.RELEASE.jar:5.1.0.RELEASE]
	at org.springframework.data.jdbc.core.DefaultDataAccessStrategy.findById(DefaultDataAccessStrategy.java:204) ~[spring-data-jdbc-1.0.0.RELEASE.jar:1.0.0.RELEASE]
...
```

## JdbcConfigurationの拡張クラス

上記エラーを解決する方法はいくつかありそうですが、本エントリでは`JdbcCustomConversions`に「`Clob`を`String`に変換する`Converter`」を追加することで解決してみようと思います。

```java:src/main/java/com/example/demo/config
package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.repository.config.JdbcConfiguration;

import java.sql.Clob;
import java.sql.SQLException;
import java.util.Collections;

@Configuration // Configurationアノテーションを付与
public class MyJdbcConfiguration extends JdbcConfiguration { // JdbcConfigurationクラスを継承

  @Override // メソッドをオーバライドし、カスタマイズしたJdbcCustomConversionsを返却する
  protected JdbcCustomConversions jdbcCustomConversions() {
    return new JdbcCustomConversions(Collections.singletonList(new Converter<Clob, String>() {
      @Override
      public String convert(Clob clob) {
        try {
          return clob == null ? null : clob.getSubString(1L, (int) clob.length());
        } catch (SQLException e) {
          throw new IllegalStateException(e);
        }
      }
    }));
  }

}
```

### アプリケーションへの適用

コンポーネントスキャン対象のパッケージにコンフィグレーションクラスを配置することで、自動でアプリケーションに適用されます。

### `@DataJdbcTest`クラスへの適用

`@DataJdbcTest`を付与したテストは基本的にコンポーネントスキャンが行われないため、明示的に作成したコンフィグレーションクラスを読み込む必要があります。

```java
package com.example.demo.repository;

import com.example.demo.config.MyJdbcConfiguration;
import com.example.demo.domain.Todo;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Optional;

@RunWith(SpringRunner.class)
@DataJdbcTest
@Import({MyJdbcConfiguration.class}) // 作成したコンフィギュレーションクラスをインポートする
public class TodoRepositoryTests {

  @Autowired
  private TodoRepository todoRepository;

  @Test
  public void saveAndFindById() {
    Todo newTodo = new Todo();
    newTodo.setTitle("飲み会");
    newTodo.setDetails("銀座 19:00");
    todoRepository.save(newTodo);

    Optional<Todo> todo = todoRepository.findById(newTodo.getId());
    Assertions.assertThat(todo.isPresent()).isTrue();
    Assertions.assertThat(todo.get().getId()).isEqualTo(newTodo.getId());
    Assertions.assertThat(todo.get().getTitle()).isEqualTo(newTodo.getTitle());
    Assertions.assertThat(todo.get().getDetails()).isEqualTo(newTodo.getDetails());
    Assertions.assertThat(todo.get().isFinished()).isFalse();
  }

}
```



# まとめ

特にまとめることはありませんが・・・Spring Boot 2.1からSpring Data JDBCのAutoConfigureとStarterがサポートされることで、Spring Data JDBCの利用者が増え、Spring Data JDBC本体の機能（＋Spring Data RESTなどのプロジェクトとの連携）が充実していくといいな〜と思っています。

# 参考ドキュメント

* https://docs.spring.io/spring-boot/docs/2.1.0.BUILD-SNAPSHOT/reference/htmlsingle/
* https://github.com/spring-projects/spring-boot/issues/14489