SpringData/Hibernate @ManyToOne非预期自动级联问题排查
先还原下你的场景:
你定义了两个实体类:
@Entity public class Thing { @ManyToOne private ThingType thingType; // 其他字段、getter/setter省略... } @Entity public class ThingType { private String name; // 其他字段、getter/setter省略... }
按照JPA的默认规则,@ManyToOne的级联操作是CascadeType.NONE,所以你本来以为:拿到Thing实例,修改它关联的ThingType的name,再调用thingRepo.save(thing),ThingType的修改不会被存到数据库里。但实际结果却刚好相反,这问题出在哪呢?
先说说你用的版本:
org.springframework.boot:spring-boot:jar:1.5.7.RELEASEorg.hibernate:hibernate-core:jar:5.0.12.Finalorg.springframework.data:spring-data-jpa:jar:1.11.7.RELEASE
核心原因:和级联无关,是持久化上下文的脏检查在搞事情!
你忽略了Hibernate最核心的机制之一——持久化上下文(Persistence Context)的脏检查(Dirty Checking),这和save方法、级联操作都没关系。具体来说有这几种常见情况:
1. 你的ThingType处于「持久化状态」
如果你的代码是在同一个事务/持久化上下文里执行的,比如:
// 同一个Spring事务中 Thing thing = thingRepo.findById(1L).orElseThrow(); ThingType thingType = thing.getThingType(); thingType.setName("新名称"); thingRepo.save(thing);
此时从thing.getThingType()拿到的ThingType实例,是被Hibernate加载到持久化上下文里的,处于Managed(被管理)状态。Hibernate会自动跟踪这个实例的所有变化,在事务提交前(不管你有没有调用save),它会对比实例的当前状态和加载时的快照,发现字段变更就自动同步到数据库——这就是脏检查的作用,和你调用save(thing)完全无关,就算你删了save那行代码,事务提交时一样会更新ThingType。
2. ThingType之前已经被加载到当前上下文
比如在同一个事务里,你之前执行过thingTypeRepo.findById(xxx),那这个ThingType已经被Hibernate缓存起来了。之后你通过thing.getThingType()拿到的是同一个实例,修改它的字段后,脏检查依然会捕捉到变更并持久化。
3. 事务边界的影响
如果你的整个操作都在一个开启的Spring事务中,那整个方法执行期间都共用同一个持久化上下文。只要实体处于Managed状态,任何字段修改都会在事务结束时被同步到数据库,和你调不调用save没有直接关系。
怎么验证和解决?
验证方法
在修改ThingType的name之后,调用entityManager.detach(thingType),把这个实例从持久化上下文里移除,再执行thingRepo.save(thing)——这时候ThingType的修改就不会被保存到数据库了,就能验证是脏检查的问题。
解决思路
如果你就是想修改关联实体但不想持久化它,可以试试这几种方法:
- 调用
entityManager.detach(thingType)将实体从持久化上下文分离,让它变成游离(Detached)状态 - 使用
EntityManager.find(ThingType.class, id, LockModeType.NONE),或者在Spring Data JPA里用@Query配合READ_ONLY注解,直接获取游离状态的实体 - 控制事务边界,比如把不需要持久化的修改逻辑放到事务外面,或者开启新的、只读的事务来获取实体
内容的提问来源于stack exchange,提问作者dann.dev

