Hibernate提交时添加实体到未初始化懒加载集合引发TransientObjectException
实体定义
Parent实体:
@Entity public class Parent extends BaseEntity { private String content; @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) private List<Child> childs = new ArrayList<>(); }
Child实体:
@Entity public class Child extends BaseEntity { @NotNull @ManyToOne private Parent parent; private int value; }
BaseEntity仅负责ID管理,无特殊逻辑;Parent与Child为懒加载一对多关系,配置了全级联操作及孤儿移除。
正常执行的代码
以下代码执行正常,事务提交时新Child会被保存:
@Transactional public void run() { var p1 = entityManager.find(Parent.class, 1L); var newChild = Child.builder() .value(999) .parent(p1) .build(); p1.getChilds().add(newChild); entityManager.merge(p1); }
触发异常的代码
若添加新Child后调用childs.size()触发集合加载,会抛出异常:
@Transactional public void run() { var p1 = entityManager.find(Parent.class, 1L); var newChild = Child.builder() .value(999) .parent(p1) .build(); p1.getChilds().add(newChild); p1.getChilds().size(); entityManager.merge(p1); }
抛出的异常信息:
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: de.lmis.lazycollection.model.Child
已知解决方法
- 在添加新Child前调用
getChilds().size() - 关闭
orphanRemoval
用户疑问
懒加载集合应保证按需获取数据,无需关注加载时机。若无法解释,需确保添加元素前集合已加载,请问理解哪里有误?
这问题的核心是Hibernate懒加载集合的代理实现和orphanRemoval=true的协同逻辑导致的:
懒加载集合的代理特性:懒加载的集合初始是个代理对象(比如
PersistentBag),而非真实的ArrayList。当直接往代理集合中添加瞬时态Child时,Hibernate会暂存这个添加操作,不会立刻校验元素状态,等后续flush或集合初始化时,再通过cascade=CascadeType.ALL自动保存新Child,所以不会报错。先添加再加载集合的冲突:如果先添加瞬时态Child,再调用
size()触发集合加载,Hibernate会把代理集合替换为从数据库查询到的真实持久化集合,同时会立即校验集合内所有元素的状态。此时新Child是瞬时态(未生成ID、未被持久化),而orphanRemoval=true的逻辑要求集合内的元素必须是与当前Parent关联的持久化对象——它会认为这个瞬时态Child是“无主”对象,因此抛出异常,因为此时集合初始化的校验动作优先于cascade的保存逻辑执行。先加载再添加的合理性:若先调用
size()加载集合,此时集合已变为真实的持久化集合,Hibernate会正常识别新添加的Child,通过cascade规则自动处理保存,不会触发校验异常。关闭orphanRemoval的作用:关闭该属性后,Hibernate不会严格校验集合元素的关联状态,仅依赖cascade规则处理,因此不会触发异常。
内容的提问来源于stack exchange,提问作者DommeL

