Spring Data JPA克隆一对多实体抛出delete-orphan集合修改异常
问题背景
使用Spring Data JPA克隆带一对多关联的实体时,编写克隆逻辑后执行抛出异常,初始实现代码如下:
fun clonePurchaseOrder(purchaseOrder: PurchaseOrder, operator: String): PurchaseOrder { //prepare data val items = purchaseOrder.items //detach items.forEach { entityManager.detach(it) } purchaseOrder.costRevise?.let { entityManager.detach(it) } entityManager.detach(purchaseOrder) purchaseOrder.id = null //modify items.forEach { it.id = null it.purchaseOrder = purchaseOrder } //modify purchaseOrder val now = Date() return purchaseOrder.apply { number = null costRevise = null paymentHistories = mutableListOf() status = PurchaseOrder.Status.DRAFT createTime = now creator = operator modifyTime = now submitTime = null submitOperator = null closeOperator = null closeReason = null closeTime = null }.save() }
抛出的异常信息:
org.springframework.orm.jpa.JpaSystemException: Don't change the reference to a collection with delete-orphan enabled : com.hkmci.web.bms2.backend.database.entity.PurchaseOrder.items; nested exception is org.hibernate.HibernateException: Don't change the reference to a collection with delete-orphan enabled : com.hkmci.web.bms2.backend.database.entity.PurchaseOrder.items
关联实体核心配置:
@Entity class PurchaseOrder( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, // 其他字段省略 @OneToMany(mappedBy = "purchaseOrder", cascade = [CascadeType.ALL], orphanRemoval = true) var items: MutableList<PurchaseOrderItem> = mutableListOf(), // 其他字段省略 )
@Entity class PurchaseOrderItem( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, @ManyToOne(fetch = FetchType.LAZY) var purchaseOrder: PurchaseOrder, // 其他字段省略 )
其中PurchaseOrder的items集合开启了orphanRemoval = true孤儿删除配置。
异常原因
- Hibernate对配置了
orphanRemoval = true的关联集合有强校验规则:集合实例必须和所属实体实例严格绑定,不允许将其他实体关联的集合实例直接赋值给当前实体的集合字段,也不允许直接替换持久化状态下实体的集合引用,否则就会抛出上述异常。 - 代码中的触发点:从原持久化
PurchaseOrder对象中取出的items集合,本质是Hibernate为原ID对应的采购单生成的专属持久化代理集合(实现类为PersistentBag等),和原采购单实例绑定。代码中仅重置了采购单和子项的ID,没有替换items字段的集合引用,而是直接把和原采购单绑定的代理集合挂到了重置ID后的“新采购单”上,触发了Hibernate的引用校验。
修复方案
核心修复逻辑:所有配置了orphanRemoval = true的集合字段,克隆时不要复用原持久化代理集合,必须创建全新的集合实例,将处理好的子项放入新集合后再赋值给实体。
修复后的代码如下:
fun clonePurchaseOrder(purchaseOrder: PurchaseOrder, operator: String): PurchaseOrder { // 提前取出原关联数据,避免后续操作影响原集合引用 val originalItems = purchaseOrder.items val originalCostRevise = purchaseOrder.costRevise // 分离所有关联实体与根实体,避免修改污染持久化上下文中原数据 originalItems.forEach { entityManager.detach(it) } originalCostRevise?.let { entityManager.detach(it) } entityManager.detach(purchaseOrder) // 重置根实体ID,标记为新实体 purchaseOrder.id = null // 处理子项:重置ID,绑定到当前新的根实体 val clonedItems = originalItems.map { item -> item.apply { id = null purchaseOrder = this@clonePurchaseOrder } }.toMutableList() val now = Date() return purchaseOrder.apply { number = null // 重点:替换为全新的集合实例,不复用原持久化代理集合 items = clonedItems costRevise = null paymentHistories = mutableListOf() status = PurchaseOrder.Status.DRAFT createTime = now creator = operator modifyTime = now submitTime = null submitOperator = null closeOperator = null closeReason = null closeTime = null }.save() }
补充说明:如果costRevise这类一对一关联也配置了orphanRemoval = true,克隆时如果需要保留该关联数据,同样不能直接复用原实体实例,需要新建实体对象拷贝属性后再关联。
内容的提问来源于stack exchange,提问作者funnyJack
相关产品推荐
相关产品推荐

