1. 简介

在之前的 JDBI 教程 中,我们介绍了 JDBI 的基本用法。JDBI 是一个开源的数据库访问库,能显著减少直接使用 JDBC 时的模板代码。

本文将重点讲解 如何在 Spring Boot 项目中集成 JDBI。同时也会分析 JDBI 在某些场景下为何是 Spring Data JPA 的一个优秀替代方案——尤其当你需要更细粒度的 SQL 控制时。

2. 项目配置

首先,我们需要引入 JDBI 的相关依赖。这次我们使用 JDBI 官方提供的 Spring 集成插件,它会自动带入核心依赖。此外,还会引入 SqlObject 插件,它提供了更强大的 DAO 接口能力:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.0.5</version>
</dependency>
<dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-spring5</artifactId>
    <version>3.38.0</version>
</dependency>
<dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-sqlobject</artifactId>
    <version>3.38.0</version> 
</dependency>

最新版本可在 Maven Central 查看:

此外,还需要一个数据库驱动。本文使用 H2 内存数据库,因此添加如下依赖:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
    <scope>runtime</scope>
</dependency>

⚠️ 注意:虽然这里引入了 spring-boot-starter-data-jpa,但我们并不会使用 JPA,仅是为了方便启动数据源配置。你也可以替换成 spring-boot-starter-jdbc 更轻量。

3. JDBI 实例化与配置

在 Spring 环境中,我们需要将 Jdbi 实例注册为 Spring Bean,以便全局使用。

借助 Spring Boot 的自动配置机制,我们可以轻松获取 DataSource,并通过 @Bean 方法创建并配置 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);

        // 注册所有插件
        log.info("[I27] Installing plugins... ({} found)", jdbiPlugins.size());
        jdbiPlugins.forEach(jdbi::installPlugin);

        // 注册所有 RowMapper
        log.info("[I31] Installing rowMappers... ({} found)", rowMappers.size());
        rowMappers.forEach(jdbi::registerRowMapper);
        return jdbi;
    }
}

✅ 关键点:

  • 使用 TransactionAwareDataSourceProxy 包装 DataSource,这是实现 JDBI 与 Spring 事务集成 的核心。
  • 自动发现并注册所有 JdbiPluginRowMapper,避免手动配置。
  • 最终返回一个“开箱即用”的 Jdbi Bean,后续可直接注入使用。

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 yearDate;
    private String sku;
    private Long makerId;
    // getters and setters ...
}

⚠️ 提示:虽然这里没有注解,但 JDBI 默认通过字段名或 getter/setter 映射数据库列,命名需遵循 JavaBean 规范。

5. 创建 DAO 接口

JDBI 的 SqlObject 插件允许我们通过接口定义 DAO,风格类似 Spring Data,但更贴近 SQL。

只需定义接口并添加注解,JDBI 会自动处理连接、Statement、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

  • ✅ 作用:告诉 JDBI SQL 语句位于 classpath 下的外部 .sql 文件中。
  • 默认路径规则:接口全限定名/方法名.sql,例如 com.example.dao.CarMakerDao/insert.sql
  • 可通过 @SqlQuery("custom.sql") 覆盖默认路径。

5.2 @SqlUpdate / @SqlBatch / @SqlQuery

注解 用途 示例
@SqlUpdate 执行 INSERT/UPDATE/DELETE insert, updateById
@SqlBatch 批量执行同一条 SQL bulkInsert 处理 List
@SqlQuery 执行 SELECT 并返回结果 findById, findAll

@SqlBatch 在处理大批量数据时性能优势明显,避免 N+1 问题。

5.3 @GetGeneratedKeys

  • ✅ 用于获取插入后数据库生成的主键(如自增 ID)。
  • 常见于 INSERT 操作后需要返回新 ID 的场景。
  • 支持单条和批量插入(返回 List<Long>)。

5.4 @BindBean / @Bind

注解 绑定方式 示例
@BindBean 将 POJO 属性映射到 SQL 命名参数 INSERT INTO car (name) VALUES (:name)
@Bind 将方法参数绑定到命名参数 :makerId, :sku

⚠️ @BindBean 支持嵌套属性(如 :owner.name),非常方便。

6. 使用 DAO

在 Spring 中,最简单的使用方式是通过 Jdbi.onDemand() 创建代理 Bean:

@Bean
public CarMakerDao carMakerDao(Jdbi jdbi) {        
    return jdbi.onDemand(CarMakerDao.class);       
}

@Bean
public CarModelDao carModelDao(Jdbi jdbi) {
    return jdbi.onDemand(CarModelDao.class);
}

✅ 优势:

  • 返回的 DAO 实例是线程安全的。
  • 每次方法调用才获取连接,调用结束自动释放。
  • TransactionAwareDataSourceProxy 配合,完美支持 Spring 事务。

⚠️ 踩坑提醒:如果项目表很多,手动写这些 @Bean 方法会很繁琐。建议封装一个通用的 JdbiDaoRegistrar 或使用 AOP 自动生成。

7. 事务性服务

下面是一个典型业务场景:保存一个 CarMaker 及其关联的多个 CarModel,要求 原子性 —— 任一失败则整体回滚。

@Service
public class CarMakerService {
    
    private final CarMakerDao carMakerDao;
    private final 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();
    }
}

✅ 关键机制:

  • 使用 Spring 的 @Transactional 而非 JDBI 的 @Transaction,确保跨资源事务一致性。
  • 所有 DAO 操作共享同一个数据库连接,由 TransactionAwareDataSourceProxy 保证。
  • 若插入某个 CarModel 失败(如唯一键冲突),整个事务回滚,数据不会残留。

💡 简单粗暴地说:只要配置正确,JDBI 就能“无缝融入”Spring 事务体系,像原生 JDBC 一样可控,又像 ORM 一样省心。

8. 总结

本文演示了如何在 Spring Boot 中高效集成 JDBI:

  • ✅ 利用 jdbi3-spring5 实现与 Spring 的深度整合
  • ✅ 通过 onDemand 创建轻量级 DAO 代理
  • ✅ 借助 TransactionAwareDataSourceProxy 实现事务透明管理
  • ✅ SQL 与代码分离,提升可维护性

当你需要比 JPA 更灵活的 SQL 控制,又不想手写 JDBC 模板代码时,JDBI 是一个非常值得考虑的选择。

所有示例代码已上传至 GitHub:https://github.com/yourname/spring-boot-jdbi-demo


原始标题:Using JDBI with Spring Boot | Baeldung