Spring Data JDBC与R2DBC详解
定位:第 05 篇,讲透 Spring Data JDBC 的轻量聚合映射模型、与 JPA 的取舍,以及 R2DBC 响应式数据访问
适用版本:Spring Data JDBC 3.x / R2DBC
目录
- [一、Spring Data JDBC 定位](#一、Spring Data JDBC 定位)
- 二、聚合映射
- [三、与 JPA 的取舍](#三、与 JPA 的取舍)
- [四、R2DBC 响应式数据访问](#四、R2DBC 响应式数据访问)
- 五、总结
- 六、常见高频面试题
一、Spring Data JDBC 定位
1.1 它是什么
Spring Data JDBC 是轻量的关系数据映射------同样是 Spring Data 家族(Repository 统一抽象),但刻意不做全量 ORM:
| 没有 | 有 |
|---|---|
| 懒加载代理 | 聚合整体加载 |
| 二级缓存 | 简单的语句级缓存之外无 |
| 持久化上下文/脏检查 | save 即写库 |
| 复杂关联导航 | 聚合内映射 |
设计思想:简单、可预测------没有代理魔法,对象是什么就是什么;读写以聚合为单位。
1.2 基本用法
java
@Table("orders")
public class Order {
@Id private Long id;
private String orderNo;
private Set<Item> items; // 从属实体:映射 order_item 表
private Address address; // 内嵌对象:映射同表列或从属表
}
public interface OrderRepository extends CrudRepository<Order, Long> {
List<Order> findByOrderNo(String orderNo);
@Query("SELECT * FROM orders WHERE order_no = :no")
Order findByNo(@Param("no") String no);
}
二、聚合映射
2.1 聚合 = 读写单位
聚合根(Order)映射主表
├── 内嵌值对象(Address)→ 主表的列(或单独从属表)
├── 集合从属实体(Set<Item>)→ 从属表(order_item)
└── save(order):根 + 从属一起持久化
2.2 规则与约束
| 规则 | 说明 |
|---|---|
| 一切随根读写 | 加载根时把从属实体一起查出;保存时一起写 |
| 无跨聚合对象引用 | 引用别的聚合只存其 ID,不持有对象 |
| 从属实体的生命周期由根管理 | 根删除,从属一并删除 |
这与 DDD 聚合概念对齐:聚合是一致性边界------边界内强一致(一次事务),边界间最终一致。
2.3 大聚合的注意
从属集合很大时(一个订单几千条明细),整体加载/删除重建从属表的代价高------此时应重新审视聚合设计(拆分聚合),这正是 Data JDBC 用"简单模型"倒逼合理建模的体现。
三、与 JPA 的取舍
| 维度 | Spring Data JDBC | JPA/Hibernate |
|---|---|---|
| 复杂度 | 低,行为直白 | 高(状态/缓存/代理) |
| 懒加载 | 无 | 有(及其坑) |
| 关联表达力 | 聚合内 | 全关联图 |
| 学习曲线 | 平缓 | 陡 |
| 适合 | 模型简单、CRUD 为主 | 领域模型复杂、需要对象图管理 |
选型直觉:如果你的 JPA 用法基本是"存/取聚合 + 少量查询",没用到懒加载/缓存/级联的复杂面------Data JDBC 能给你同样的能力而少一堆坑。两者不是替代关系,是复杂度谱系上的不同位置(呼应 ORM 目录选型篇)。
四、R2DBC 响应式数据访问
4.1 定位
JDBC:阻塞式关系库访问(每操作占线程)
R2DBC:响应式非阻塞规范(Reactive Relational Database Connectivity)
WebFlux 全链路非阻塞要求数据层也非阻塞------传统 JDBC 会阻塞 EventLoop(WB-07 铁律),R2DBC 是关系库的解。
4.2 两种访问方式
java
// Repository(返回 Mono/Flux)
public interface UserRepository extends ReactiveCrudRepository<User, Long> { }
// DatabaseClient(编程式,类模板)
databaseClient.sql("SELECT * FROM users WHERE age > :age")
.bind("age", 18)
.map(row -> new User(...))
.all(); // Flux<User>
4.3 事务与生态
响应式事务:ReactiveTransactionManager(上下文经 Reactor Context 传播,WB-07)
驱动支持:PostgreSQL、MySQL、MariaDB、Oracle、SQL Server 等
注意:不是所有库/功能都有响应式驱动(如某些高级特性)
4.4 适用判断
用 R2DBC:WebFlux 应用 + 需要关系库 + 追求端到端非阻塞
不用:MVC 同步应用(无收益)、阻塞依赖多的系统(木桶效应)
五、总结
- Data JDBC:轻量关系映射------无懒加载/缓存/持久化上下文,行为直白;聚合为读写单位,从属实体随根存取,跨聚合只存 ID。
- 与 JPA 取舍:模型简单、CRUD 为主选 Data JDBC 避坑;复杂对象图与关联选 JPA;两者在复杂度谱系上各居其位。
- 聚合思想:一致性边界内一次事务,边界间最终一致;大聚合是建模问题的信号。
- R2DBC:关系库的响应式驱动规范;Repository/DatabaseClient 返回 Mono/Flux;配响应式事务;WebFlux 全链路非阻塞的数据层方案。
六、常见高频面试题
1. Spring Data JDBC 和 Spring Data JPA 的区别?
要点:复杂度谱系不同。Data JDBC 是轻量映射:没有懒加载、二级缓存、持久化上下文/脏检查,save 即写库,对象无代理魔法,行为直白可预测;按聚合整体读写。Data JPA 背后是 Hibernate 全量 ORM:对象图、状态管理、懒加载、缓存、复杂关联俱全,能力强但复杂。选择:模型简单 CRUD 为主选 Data JDBC 少踩坑;领域模型复杂需要对象图能力选 JPA。
2. Spring Data JDBC 的聚合映射是怎样的?
要点:以聚合为读写单位。聚合根映射主表;内嵌值对象映射为主表列或从属表;集合从属实体映射从属表;加载根时把从属实体一起查出,save 时一起持久化,删根一并删从属。约束:跨聚合不持有对象引用,只存 ID。这与 DDD 聚合对齐------聚合是一致性边界。注意大聚合(从属集合巨大)会导致整体加载/重写开销大,是拆分聚合的信号。
3. 为什么 Spring Data JDBC 不支持懒加载?这是缺陷吗?
要点:这是设计取舍不是缺陷。懒加载带来代理、会话依赖、N+1、序列化触发等一系列复杂性(ORM 目录详述);Data JDBC 选择"一次加载聚合整体"的简单模型------对象就是数据本身,无代理无会话,任何时刻访问属性都安全。代价是大聚合加载成本,解法是合理设计聚合(小而内聚)。哲学:用模型约束换取行为可预测,避免 ORM 的隐藏复杂度。
4. 什么场景适合用 Spring Data JDBC?
要点:① 模型以聚合整体存取为主(CRUD 为主,少量查询);② 不需要懒加载、二级缓存、级联等 ORM 高级特性;③ 希望数据访问行为直白、排查简单;④ 新服务或重构时愿意按聚合建模。不适合:复杂关联导航、需要细粒度延迟加载的大对象图、大量跨表复杂查询(这类场景 JPA/MyBatis 更合适)。本质是"用不到的复杂度不要背"。
5. R2DBC 是什么?和 JDBC 的关系?
要点:R2DBC 是关系型数据库的响应式访问规范(Reactive Relational Database Connectivity),对应 JDBC 之于阻塞世界。JDBC 调用阻塞当前线程直到数据库响应;R2DBC 返回 Publisher(Mono/Flux),IO 等待不占线程,配合事件循环实现高并发。Spring 提供 Spring Data R2DBC(ReactiveCrudRepository、DatabaseClient)。需要数据库驱动支持(各数据库有响应式驱动)。用于 WebFlux 全链路非阻塞场景。
6. WebFlux 应用访问关系库有哪些选择?
要点:① R2DBC------端到端非阻塞,首选:Spring Data R2DBC 仓库或 DatabaseClient,配响应式事务;② 传统 JDBC 隔离------用 subscribeOn(boundedElastic) 把阻塞调用挪到弹性线程池(妥协方案,仍消耗线程);③ 数据服务化------调专门的数据服务(HTTP/gRPC 非阻塞客户端)。选择取决于非阻塞诉求与驱动支持:驱动完备且追求性能选 R2DBC,改造成本权衡下可用隔离方案过渡。
7. 响应式事务和 JPA 事务有什么区别?
要点:绑定机制不同。JPA/传统事务把连接绑定当前线程(ThreadLocal),方法链天然共享;响应式执行跨线程切换,ThreadLocal 失效------响应式事务(ReactiveTransactionManager)通过 Reactor Context 随订阅传播事务上下文,连接按上下文获取。使用上仍是 @Transactional(响应式支持),但要注意:上下文只在响应式链内传播,切到普通线程池要显式传递;混用阻塞代码会破坏传播。
8. DatabaseClient 和 R2DBC Repository 怎么选?
要点:Repository 声明式------继承 ReactiveCrudRepository 获得 CRUD 与派生查询,返回 Mono/Flux,适合标准存取;DatabaseClient 编程式------sql().bind().map() 完全控制语句与映射,适合复杂查询、动态语句、部分更新。与 JDBC 世界的 Template/Repository 分工一致。同一应用可混用:标准操作用 Repository,复杂场景用 DatabaseClient。
9. 项目用 JPA 很多年了,值得换 Data JDBC 吗?
要点:一般不值得整体迁移,按新服务评估。存量 JPA 系统重写成本高、风险大,且已积累的 ORM 经验与调优是资产;除非有强动机(复杂性问题严重、团队要简化技术栈)。新服务/新模块可以评估:模型简单聚合化、不需要懒加载缓存,用 Data JDBC 更轻。原则:按复杂度谱系为新场景选合适的工具,而不是对存量做无收益的替换。
10. 响应式数据访问的常见坑有哪些?
要点:① 链上混入阻塞调用(同步驱动/阻塞库)------EventLoop 被占,全局吞吐崩,必须隔离或换响应式实现;② 忘记订阅------惰性流不触发;③ 上下文丢失------事务/安全上下文在切换调度器或普通线程池后失效;④ 背压处理不当------大量查询结果不做限制拉爆内存(要分页/限流);⑤ 调试困难------异步堆栈不连贯,需断点与日志策略。响应式的收益伴随这些纪律成本,团队要评估。
