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

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孤儿删除配置。

异常原因
  1. Hibernate对配置了orphanRemoval = true的关联集合有强校验规则:集合实例必须和所属实体实例严格绑定,不允许将其他实体关联的集合实例直接赋值给当前实体的集合字段,也不允许直接替换持久化状态下实体的集合引用,否则就会抛出上述异常。
  2. 代码中的触发点:从原持久化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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 12:21:28