1. 简介
在之前的教程中,我们介绍了JDBI的基础知识,这是一个用于关系数据库访问的开源库,它删除了与直接使用 JDBC 相关的大部分样板代码。
这一次,我们将看到如何在 Spring Boot 应用程序中使用 JDBI。我们还将介绍该库的某些方面,使其在某些情况下成为 Spring Data JPA 的良好替代品。
2. 项目设置
首先,让我们将适当的 JDBI 依赖项添加到我们的项目中。这一次,我们将使用 JDBI 的 Spring 集成插件,它带来了所有必需的核心依赖项。我们还将引入 SqlObject 插件,它为我们将在示例中使用的基本 JDBI 添加了一些额外的功能:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-spring4</artifactId>
<version>3.9.1</version>
</dependency>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-sqlobject</artifactId>
<version>3.9.1</version>
</dependency>
这些工件的最新版本可以在 Maven Central 中找到:
我们还需要一个合适的 JDBC 驱动程序来访问我们的数据库。在本文中,我们将使用H2,因此我们还必须将其驱动程序添加到我们的依赖项列表中:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.199</version>
<scope>runtime</scope>
</dependency>
3. JDBI 实例化和配置
我们在之前的文章中已经看到,我们需要一个Jdbi实例作为访问 JDBI API 的入口点。由于我们处于 Spring 世界中,因此将此类的实例作为 bean 提供是有意义的。
我们将利用 Spring Boot 的自动配置功能来初始化DataSource并将其传递给@Bean -annotated 方法,该方法将创建我们的全局Jdbi实例。
我们还将任何发现的插件和RowMapper实例传递给此方法,以便预先注册它们:
@Configuration
public class JdbiConfiguration {
@Bean
public Jdbi jdbi(DataSource ds, List<JdbiPlugin> jdbiPlugins, List<RowMapper<?>> rowMappers) {
TransactionAwareDataSourceProxy proxy = new TransactionAwareDataSourceProxy(ds);
Jdbi jdbi = Jdbi.create(proxy);
jdbiPlugins.forEach(plugin -> jdbi.installPlugin(plugin));
rowMappers.forEach(mapper -> jdbi.registerRowMapper(mapper));
return jdbi;
}
}
在这里,我们使用可用的DataSource并将其包装在TransactionAwareDataSourceProxy中。我们需要这个包装器,以便将 Spring 管理的事务与 JDBI 集成,我们将在后面看到。
注册插件和 RowMapper 实例很简单。我们所要做的就是分别为每个可用的 JdbiPlugin和RowMapper 调用installPlugin和installRowMapper。之后,我们有一个完全配置的 Jdbi实例,我们可以在我们的应用程序中使用它。
4. 样本域
我们的示例使用了一个非常简单的领域模型,它只包含两个类:CarMaker和CarModel。由于 JDBI 不需要对我们的域类进行任何注释,因此我们可以使用简单的 POJO:
public class CarMaker {
private Long id;
private String name;
private List<CarModel> models;
// getters and setters ...
}
public class CarModel {
private Long id;
private String name;
private Integer year;
private String sku;
private Long makerId;
// getters and setters ...
}
5. 创建 DAO
现在,让我们为我们的域类创建数据访问对象 (DAO)。JDBI SqlObject 插件提供了一种简单的方法来实现这些类,这类似于 Spring Data 处理这个主题的方式。
我们只需要定义一个带有一些注释的接口,JDBI 将自动处理所有低级的东西,例如处理 JDBC 连接和创建/处理语句和ResultSet:
@UseClasspathSqlLocator
public interface CarMakerDao {
@SqlUpdate
@GetGeneratedKeys
Long insert(@BindBean CarMaker carMaker);
@SqlBatch("insert")
@GetGeneratedKeys
List<Long> bulkInsert(@BindBean List<CarMaker> carMakers);
@SqlQuery
CarMaker findById(Long id);
}
@UseClasspathSqlLocator
public interface CarModelDao {
@SqlUpdate
@GetGeneratedKeys
Long insert(@BindBean CarModel carModel);
@SqlBatch("insert")
@GetGeneratedKeys
List<Long> bulkInsert(@BindBean List<CarModel> models);
@SqlQuery
CarModel findByMakerIdAndSku(@Bind("makerId") Long makerId, @Bind("sku") String sku );
}
这些接口都有大量注释,所以让我们快速浏览一下它们。
5.1。@UseClasspathSqlLocator
@UseClasspathSqlLocator注释告诉JDBI与每个方法关联的实际 SQL 语句位于外部资源文件中。默认情况下,JDBI 将使用接口的完全限定名称和方法来查找资源。例如,给定带有findById()方法的abcFoo接口的 FQN ,JDBI 将查找名为a/b/c/Foo/findById.sql 的资源。
通过将资源名称作为@SqlXXX注释的值传递,可以为任何给定方法覆盖此默认行为。
5.2. @SqlUpdate/@SqlBatch/@SqlQuery
我们使用@SqlUpdate、 @SqlBatch和@SqlQuery注释来标记数据访问方法,这些方法将使用给定的参数执行。这些注释可以采用可选的字符串值,这将是要执行的文字 SQL 语句——包括任何命名参数——或者当与@UseClasspathSqlLocator一起使用时,包含它的资源名称。
@SqlBatch -annotated 方法可以具有类似集合的参数,并为单个批处理语句中的每个可用项执行相同的 SQL 语句。在上述每个 DAO 类中,我们都有一个bulkInsert 方法来说明其用法。使用批处理语句的主要优点是我们在处理大型数据集时可以获得的附加性能。
5.3. @GetGeneratedKeys
顾名思义,@ GetGeneratedKeys注释允许我们恢复成功执行后生成的任何密钥。它主要用于 插入语句中,我们的数据库将自动生成新的标识符,我们需要在代码中恢复它们。
5.4. @BindBean/@Bind
我们使用@BindBean和@Bind注解将SQL 语句中的命名参数与方法参数进行绑定。@BindBean使用标准 bean 约定从 POJO 中提取属性——包括嵌套的属性。@Bind使用参数名称或提供的值将其值映射到命名参数。
6. 使用 DAO
要在我们的应用程序中使用这些 DAO,我们必须使用 JDBI 中可用的一种工厂方法来实例化它们。
在 Spring 上下文中,最简单的方法是使用onDemand方法为每个 DAO 创建一个 bean :
@Bean
public CarMakerDao carMakerDao(Jdbi jdbi) {
return jdbi.onDemand(CarMakerDao.class);
}
@Bean
public CarModelDao carModelDao(Jdbi jdbi) {
return jdbi.onDemand(CarModelDao.class);
}
onDemand创建的实例是线程安全的,并且仅在方法调用期间使用数据库连接。由于 JDBI 我们将使用提供的TransactionAwareDataSourceProxy,这意味着我们可以将它与 Spring 管理的事务无缝地使用。
虽然简单,但当我们必须处理多个表时,我们在这里使用的方法远非理想。避免编写此类样板代码的一种方法是创建自定义BeanFactory。 但是,描述如何实现这样的组件超出了本教程的范围。
7. 交易服务
让我们在一个简单的服务类中使用我们的 DAO 类,该类在给定填充有模型的CarMaker的情况下创建一些 CarModel实例 。首先,我们将检查给定的CarMaker之前是否已保存,如果需要,将其保存到数据库中。然后,我们将一个一个地插入每个CarModel。
如果在任何时候都存在唯一键违规(或其他错误),则整个操作必须失败并且应该执行完全回滚。
JDBI 提供了一个@Transaction注释,但我们不能在这里使用它,因为它不知道可能参与同一业务事务的其他资源。相反,我们将在我们的服务方法中使用 Spring 的@Transactional注释:
@Service
public class CarMakerService {
private CarMakerDao carMakerDao;
private CarModelDao carModelDao;
public CarMakerService(CarMakerDao carMakerDao,CarModelDao carModelDao) {
this.carMakerDao = carMakerDao;
this.carModelDao = carModelDao;
}
@Transactional
public int bulkInsert(CarMaker carMaker) {
Long carMakerId;
if (carMaker.getId() == null ) {
carMakerId = carMakerDao.insert(carMaker);
carMaker.setId(carMakerId);
}
carMaker.getModels().forEach(m -> {
m.setMakerId(carMaker.getId());
carModelDao.insert(m);
});
return carMaker.getModels().size();
}
}
该操作的实现本身非常简单:我们使用标准约定,即id字段中的空值意味着该实体尚未持久化到数据库中。如果是这种情况,我们使用构造函数中注入的CarMakerDao实例在数据库中插入一条新记录并获取生成的 id。
一旦我们有了CarMaker的 id,我们就会遍历模型, 为每个模型设置makerId 字段,然后再将其保存到数据库中。
所有这些数据库操作都将使用相同的底层连接发生,并且将是同一事务的一部分。这里的诀窍在于我们使用TransactionAwareDataSourceProxy将 JDBI 绑定到 Spring并创建 onDemand DAO。当 JDBI 请求一个新的Connection时,它将获得一个与当前事务相关联的现有的,从而将其生命周期集成到可能注册的其他资源中。
8. 结论
在本文中,我们展示了如何将 JDBI 快速集成到 Spring Boot 应用程序中。在我们由于某种原因不能使用 Spring Data JPA 但仍想使用所有其他功能(如事务管理、集成等)的场景中,这是一个强大的组合。
像往常一样,所有代码都可以在 GitHub 上找到。