昨日(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)を試してみた
」をみてみてください。
検証バージョン
- Spring Data JDBC 1.0.0.RELEASE
- Spring Boot Starter Data JDBC 2.1.0.BUILD-SNAPSHOT (2018/09/22時点)
検証プロジェクト
- 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です)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
こうすると・・・spring-data-jdbcやspring-jdbcが依存アーティファクトに追加されAutoConfigure対象になります。
Spring Data JDBCのAutoConfigure
まずは、Spring Data JDBCのAutoConfigureを使って(=設定レスで)簡単なプログラムを動かしてみます。
テーブル
テーブルを作ります。
CREATE TABLE IF NOT EXISTS todo (
id IDENTITY
,title VARCHAR NOT NULL
,details VARCHAR
,finished BOOLEAN NOT NULL
);
ドメインオブジェクト
ドメインオブジェクトを作ります。
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操作を提供するリポジトリを作ります。
package com.example.demo.repository;
import org.springframework.data.repository.CrudRepository;
import com.example.demo.domain.Todo;
public interface TodoRepository extends CrudRepository<Todo, Integer> {
}
デモアプリ
リポジトリのメソッドを使用して、ドメインオブジェクトを保存し、保存したドメインオブジェクトを参照する処理を実装します。
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());
};
}
}
デモアプリを起動すると、以下のような標準出力が出ます。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: 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とは直接関係ありませんが、デモアプリのテストも用意してみます。
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
を使うと、リポジトリ単体でテストが簡単+効率的に実施することができます。
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コンテナに登録されてしまいます(=単体テストとしては非効率な環境)。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: 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)の有効・無効を制御するプロパティが用意されています。
# 機能を無効化したい場合は 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
にすると、
CREATE TABLE IF NOT EXISTS todo (
id IDENTITY
,title TEXT NOT NULL -- TEXTへ変更
,details TEXT -- TEXTへ変更
,finished BOOLEAN NOT NULL
);
デフォルトだとClob
をString
に変換できずに以下のようなエラーが発生します。
...
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
」を追加することで解決してみようと思います。
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
を付与したテストは基本的にコンポーネントスキャンが行われないため、明示的に作成したコンフィグレーションクラスを読み込む必要があります。
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などのプロジェクトとの連携)が充実していくといいな〜と思っています。