尝试使用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
广告
将在 10 秒后关闭
bannerAds