尝试使用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。
请参考”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 INITIALIZR上,
-
- Spring Bootのバージョンを「2.1.0(SNAPSHOT)」に選択
- Dependenciesに「H2」「JDBC」を追加
根据需要,进行下载和解压缩(必要时导入IDE)。
很遗憾,目前在编写本条目时还没有解决对spring-boot-starter-data-jdbc的依赖关系,所以将依赖于spring-boot-starter-jdbc的部分修改为依赖于spring-boot-starter-data-jdbc(也可以进行添加)。
<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的自动配置
首先,我们将尝试使用Spring Data JDBC的AutoConfigure功能(没有需要进行额外设置的情况下),来运行一个简单的程序。
桌子 (zhuō zi)
我要制作一张桌子。
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
通过使用@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)来标记,可以实现与应用程序在运行时相同的配置(包括连接池和连接目标等),而不是使用内存中的内置数据库。
自动配置的定制化
目前,有一个属性可用来控制存储库的扫描功能(即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的情况下,必须通过继承类并重写方法的方式进行)。
请注意:
有些组件可以通过在DI容器中使用@Bean方法等将其注册到DI容器中,Spring Data JDBC会自动检测这些组件。例如……
DataAccessStrategy
NamingStrategy等等。
另外,如果想要自定义存储库相关的配置,可以在配置类上添加@EnableJdbcRepositories注解,并自定义属性值。
本篇介绍了如何在Spring Data JDBC中添加一个Converter(Clob -> String),以支持不受支持的数据类型。
在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的转换器”来解决。
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等项目的协作)将得到进一步完善,这是我希望看到的。
参考文档
-
- https://docs.spring.io/spring-boot/docs/2.1.0.BUILD-SNAPSHOT/reference/htmlsingle/
- https://github.com/spring-projects/spring-boot/issues/14489