掌握 Spring Data Relational 聚合根:一张订单表如何帮你避开 5 个领域设计坑
掌握 Spring Data Relational 聚合根一张订单表如何帮你避开 5 个领域设计坑【免费下载链接】spring-data-relationalSpring Data Relational. Home of Spring Data JDBC and Spring Data R2DBC.项目地址: https://gitcode.com/gh_mirrors/sp/spring-data-relationalSpring Data Relational 是 Spring Data JDBC 与 Spring Data R2DBC 共同的母项目它以领域驱动设计DDD中的聚合根为一切持久化操作的出发点。本文用一个订单删不掉的真实事故开场帮你彻底搞懂聚合根到底在解决什么以及如何用它绕开新手最容易踩的 5 个设计坑。一、先讲个事故你的订单系统为什么上线三个月就删不动了你刚接手一个订单模块代码看起来挺正常一张order表一张order_item明细表DAO 里手写 SQL业务逻辑散落在 Service 层。直到某天产品说帮我把这单删了你在测试环境试了一下数据库直接甩给你一句外键约束异常。再往后问题接踵而至订单明细改了但订单上的商品数量合计字段没跟着变两个同事同时改同一单后提交的人把先提交的改动悄悄覆盖了有人图省事直接用 SQL 客户端改了order_item结果业务缓存里永远是一份幽灵数据。你开始怀疑自己是不是 SQL 写错了表结构设计错了其实都不是。真正的问题在于——你的对象模型和表模型之间一直缺了一层叫做聚合根的胶水。二、追根溯源对象认物以类聚数据库认各行其是关系型数据库天生认表不认对象而你的业务代码天生认对象不认表。这两套世界观打架才是所有混乱的源头。打个比方。把聚合想象成一家公司的项目组聚合根就是组长。组内成员之间的协作必须通过组长来协调组外的人想找组里任何人办事也只能先找组长由组长去安排。组长对外只有一个公开的身份——工号也就是主键 ID。这样一来公司不会因为组内成员私下乱串而乱套责任永远清晰。把这个比喻翻译成技术语言聚合根要解决的根本问题有三个根本问题通俗解释没有它会怎样一致性边界一组实体必须同生共死状态一起变明细改了合计字段却是旧的原子操作一次业务动作落在多张表上要么全成、要么全不成删到一半失败留下一堆孤儿数据身份标识只有根拥有全局 ID子实体只在聚合内有名字到处直接引用子实体外键关系乱成一团Spring Data Relational 的官方文档见仓库src/main/antora/modules/ROOT/pages/jdbc/domain-driven-design.adoc开篇就点破了这一点聚合是一组保证在原子变更之间保持一致的实体集合最经典的例子正是Order和它的OrderItem。而且文档特别强调——每个聚合有且只有一个根对聚合的一切操作都必须经过这个根。三、核心机制你只调了一个 save()框架背后排了张施工清单理解了为什么再看怎么做就顺了。Spring Data Relational 的整个持久化链路就是围绕聚合根展开的一条流水线。你写的代码永远是这句orderRepository.save(order);但框架在背后做的事情远比你想象得多。它先把整个聚合的改动打包成一个AggregateChange对象——这个对象记录着这次操作是保存还是删除对应SAVE和DELETE两种Kind以及一连串的DbAction。每一个DbAction都对应一条具体的数据库操作根表要 insert 还是 update子表要插入哪些行、更新哪些行、删除哪些行。执行时框架按顺序把这张施工清单跑完再顺手把自增主键回填到你的实体上。在这条链路上有一道很关键的安检门。在DefaultRootAggregateChange.setRoot(...)这个方法里框架会校验传入的对象类型不匹配就抛异常报错信息直白得很String.format(AggregateRoot must be of type %s, entityType.getName())这套实现放在spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/目录下AggregateChange、DbAction、DefaultRootAggregateChange一字排开。翻译成人话就是框架只认聚合根不认散兵游勇。你塞给它一个不在聚合树上的对象它直接拒绝开工。为什么敢这么干因为 Spring Data Relational 有一个强约定凡是从聚合根出发能到达的所有对象都被视为这个聚合的成员归同一个 Repository 管。所以每个聚合根配一个JdbcRepositoryR2DBC 那边就是R2dbcRepository一个仓库负责一整棵对象树绝不让子实体单独暴露在外面。四、实战对照同样一张订单表两种写法两种命运光讲道理不够来做个面对面对比。下面这张表左边是正确姿势右边是翻车姿势就差在一念之间。场景✅ 正确做法❌ 翻车做法订单与明细把OrderItem放进Order聚合一个OrderRepository管到底为OrderItem单独建仓库业务代码里分开 save一致性全靠自觉订单与客户Order里只存一个customerId需要时再去查直接把Customer对象塞进Order聚合——删订单时把用户数据也一起处理掉值对象 vs 实体地址、金额这类值对象用Embedded内嵌到同一张表给每个值对象都建表、都建仓库并发修改在聚合根上挂Version乐观锁保护整棵聚合树只在明细表上加版本字段根表被覆盖了都不知道再看一段教科书式的聚合写法注意子实体身上不需要也不应该有全局主键Table(orders) class Order { Id Long id; String customerId; // 跨聚合只存 ID不存对象 Version Long version; // 乐观锁放在根上 ListOrderItem items new ArrayList(); // 聚合内实体 void addItem(String sku, int count) { items.add(new OrderItem(sku, count)); } }保存时你只需要orderRepository.save(order)框架会自己决定订单表 update明细表按需 insert 或 delete最终让数据库里的状态和对象图完全对齐。你只管维护对象数据库的脏活交给聚合根这条流水线。五、避坑清单这 5 个坑踩过任何一个都够你加班到深夜结合源码和真实项目反馈我帮你把高频翻车点一次性列全子实体可能被删除重建。当前 JDBC 实现里被聚合根引用的实体在保存时常被整体删除再重建。所以别在子表上挂自己的外键、触发器或审计逻辑否则性能和一地报错都会找上你。版本号只认根上的。Version加在聚合根上才有意义它保证的是整个聚合的乐观锁而不是某一行。聚合别贪大。能把订单和订单项放一起不代表能把半个业务系统都塞进去。聚合越大一次保存生成的 SQL 条数越多锁的范围越广性能越难看。JDBC 和 R2DBC 要遵守同一套纪律。换成响应式版spring-data-r2dbc聚合根、仓库、边界的约定完全一致别因为换了个异步 API 就放飞自我。跨聚合只准留电话号码。引用别的聚合只存它的 ID别存对象、别直接调它的子实体——这是保证边界不腐化的底线。想验证自己的边界设计对不对仓库里那些测试类就是现成的试金石比如JdbcRepositoryEmbeddedNotInAggregateRootIntegrationTests专门测试非聚合根内部的嵌入式对象该怎么处理AbstractJdbcAggregateTemplateIntegrationTests里则覆盖了带引用实体、带版本控制的保存删除场景。照着它们的样子写自己的边界测试比看十篇博客都管用。六、收个尾聚合根不是数学题是一份谁对谁负责的契约回头再看开头的订单事故你其实只缺一个思维转变别把数据库当成对象模型的备份而是把聚合当成数据库操作的最小单位。聚合根帮你把一致性、原子性和身份标识这三件事打包解决让你从手工维护多张表的隐式同步里彻底解放出来。下一步怎么走给你一条渐进路线通读仓库里src/main/antora/modules/ROOT/pages/jdbc/下的domain-driven-design.adoc把约定原文看一遍打开spring-data-relational的core/conversion包顺着AggregateChange把施工清单的生成过程读通挑一个你手头最痛的表结构按本文的对照表重构成聚合再配一组边界测试。从一张订单表开始把聚合根用顺手你会发现自己写的代码从能跑变成了敢改——这大概是领域驱动设计给你最好的礼物。【免费下载链接】spring-data-relationalSpring Data Relational. Home of Spring Data JDBC and Spring Data R2DBC.项目地址: https://gitcode.com/gh_mirrors/sp/spring-data-relational创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考