Hibernate 6.x升级后未改动空集合触发孤儿移除异常
更新
已在Hibernate追踪器提交问题HHH-15512。
问题背景
将Hibernate从5.x升级至6.1.2.Final后,一个参数化测试用例执行四次,其中两次提交事务时抛出错误:
A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance
测试仅修改childOnes集合,但异常指向从未被操作的childTwos集合。
实体代码
public class MyEntity { @LazyCollection(LazyCollectionOption.TRUE) @Cascade(CascadeType.ALL) @OnDelete(action = OnDeleteAction.CASCADE) @OneToMany(orphanRemoval = true, mappedBy = "parent") private final List<ChildOne> childOnes = new ArrayList<>(); @LazyCollection(LazyCollectionOption.TRUE) @Cascade(CascadeType.ALL) @OnDelete(action = OnDeleteAction.CASCADE) @OneToMany(orphanRemoval = true, mappedBy = "parent") private final List<ChildTwo> childTwos = new ArrayList<>(); }
测试代码(简化版)
@BeforeEach void openDatabaseSession() { dbTx = sfp.beginTransaction(); } @AfterEach void closeDatabaseSession() { dbTx.commit(); } @ParameterizedTest @MethodSource("irrelevantSource") void doTest(/* some irrelevant params */) { final var child = new ChildOne(); // given final var e = new MyEntity(); e.addChildOne(child); sfp.getCurrentSession().save(e); // when final var result = service.someMethod(e); // 未修改childTwos! // then final var testee = sfp.getCurrentSession().find(MyEntity.class, e.getId()); assertNotNull(testee); var c = testee.getChildOnes(); // ... 一些断言 ... }
额外调试信息
即使移除重新加载实体的testee部分,直接使用原e仍会报错。调试Hibernate的Collections.java代码发现,实体状态为Status.MANAGED,这是异常的根源。
疑问
- 为何Hibernate会对未触碰的空集合执行孤儿移除操作?
- 为何仅四次测试中的两次出现报错?
- 如何解决该问题?不想移除
orphanRemoval=true,避免手动删除孤儿的繁琐操作。
解决方案分析
问题根源推测
Hibernate 6.x对集合代理的处理逻辑和5.x有差异,尤其是针对final修饰的空集合。当实体处于MANAGED状态时,Hibernate会尝试将集合替换为代理对象,但final字段无法被重新赋值,导致集合引用不一致,触发孤儿移除检查报错。测试偶发是因为集合懒加载时机不同——部分测试中childTwos被触发懒加载代理,部分没有,从而出现随机报错的情况。
可行解决方法
移除集合字段的
final修饰符
Hibernate需要替换集合为代理实例,final会阻止这个操作,导致引用校验失败。修改实体代码:@OneToMany(orphanRemoval = true, mappedBy = "parent") // 保留其他注解 private List<ChildOne> childOnes = new ArrayList<>(); @OneToMany(orphanRemoval = true, mappedBy = "parent") // 保留其他注解 private List<ChildTwo> childTwos = new ArrayList<>();同时可以在getter方法中返回不可修改视图(比如
Collections.unmodifiableList(childOnes)),避免外部直接修改集合引用,兼顾安全性。显式初始化集合代理
在测试或实体加载后,主动触发未使用集合的懒加载,让Hibernate完成代理替换,避免提交时的引用检查失败。比如在save(e)后添加:Hibernate.initialize(e.getChildTwos());这属于临时 workaround,后续官方修复后可移除。
升级Hibernate版本
既然已提交HHH-15512,后续官方修复后升级到对应版本即可解决该bug,可关注该问题的修复进度,优先选择升级到包含修复的版本。
偶发报错的解释
四次测试中两次报错是因为懒加载的随机性——当测试中某些路径触发了childTwos的代理初始化(比如Hibernate内部的状态检查、缓存操作),集合引用被正确替换为代理,就不会报错;如果没有触发初始化,提交事务时Hibernate尝试替换final集合失败,就会抛出异常。
内容的提问来源于stack exchange,提问作者UniversE

