You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Hibernate 6.x升级后未改动空集合触发孤儿移除异常

Hibernate 6.1.2.Final 升级后未触碰集合触发孤儿移除异常问题

更新

已在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被触发懒加载代理,部分没有,从而出现随机报错的情况。

可行解决方法

  1. 移除集合字段的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)),避免外部直接修改集合引用,兼顾安全性。

  2. 显式初始化集合代理
    在测试或实体加载后,主动触发未使用集合的懒加载,让Hibernate完成代理替换,避免提交时的引用检查失败。比如在save(e)后添加:

    Hibernate.initialize(e.getChildTwos());
    

    这属于临时 workaround,后续官方修复后可移除。

  3. 升级Hibernate版本
    既然已提交HHH-15512,后续官方修复后升级到对应版本即可解决该bug,可关注该问题的修复进度,优先选择升级到包含修复的版本。

偶发报错的解释

四次测试中两次报错是因为懒加载的随机性——当测试中某些路径触发了childTwos的代理初始化(比如Hibernate内部的状态检查、缓存操作),集合引用被正确替换为代理,就不会报错;如果没有触发初始化,提交事务时Hibernate尝试替换final集合失败,就会抛出异常。


内容的提问来源于stack exchange,提问作者UniversE

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 22:10:31