防止 Hibernate 在合并具有实体关联且 orphanRemoval 设置为 true 的实体时删除孤立实体

Prevent Hibernate from deleting orphaned entities while merging an entity having entity associations with orphanRemoval set to true

举一个非常简单的一对多关系的例子(国家->州)。

国家(反面):

@OneToMany(mappedBy = "country", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<StateTable> stateTableList=new ArrayList<StateTable>(0);

StateTable(拥有方):

@JoinColumn(name = "country_id", referencedColumnName = "country_id")
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
private Country country;

尝试更新活动数据库事务(JTA 或本地资源)中提供的(分离的)StateTable 实体的方法:

public StateTable update(StateTable stateTable) {

    // Getting the original state entity from the database.
    StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
    // Get hold of the original country (with countryId = 67, for example).
    Country oldCountry = oldState.getCountry();
    // Getting a new country entity (with countryId = 68) supplied by the client application which is responsible for modifying the StateTable entity.
    // Country has been changed from 67 to 68 in the StateTable entity using for example, a drop-down list.
    Country newCountry = entityManager.find(Country.class, stateTable.getCountry().getCountryId());
    // Attaching a managed instance to StateTable.
    stateTable.setCountry(newCountry);

    // Check whether the supplied country and the original country entities are equal.
    // (Both not null and not equal - 
    if (ObjectUtils.notEquals(newCountry, oldCountry)) {
        // Remove the state entity from the inverse collection held by the original country entity.
        oldCountry.remove(oldState);
        // Add the state entity to the inverse collection held by the newly supplied country entity
        newCountry.add(stateTable);
    }

    return entityManager.merge(stateTable);
}

需要注意的是orphanRemoval设置为trueStateTable 实体由客户端应用程序提供,该应用程序有兴趣将 StateTable 中的实体关联 Country (countryId = 67) 更改为其他内容 (countryId = 68) (因此,在 JPA 的反面,将子实体从其父实体(集合)迁移到另一个父实体(集合),orphanRemoval=true 反过来会反对)。

Hibernate 提供程序发出 DELETE DML 语句,导致从基础数据库 table.

中删除对应于 StateTable 实体的行

尽管 orphanRemoval 设置为 true,但我希望 Hibernate 发出常规 UPDATE DML 语句,导致 orphanRemoval 的效果在这完全是因为关系 link 被迁移(而不是简单地删除)。

EclipseLink 可以完成这项工作。它在给定的场景中发出 UPDATE 语句(与 orphanRemoval 设置为 true 的关系相同)。

哪个行为符合规范?在这种情况下是否可以让 Hibernate 发出一个 UPDATE 语句而不是从反面删除 orphanRemoval


这只是为了使双方的双向关系更加一致。

如有必要,在Country实体中定义上述代码段中使用的防御性link管理方法即add()remove()如下

public void add(StateTable stateTable) {
    List<StateTable> newStateTableList = getStateTableList();

    if (!newStateTableList.contains(stateTable)) {
        newStateTableList.add(stateTable);
    }

    if (stateTable.getCountry() != this) {
        stateTable.setCountry(this);
    }
}

public void remove(StateTable stateTable) {
    List<StateTable> newStateTableList = getStateTableList();

    if (newStateTableList.contains(stateTable)) {
        newStateTableList.remove(stateTable);
    }
}


更新:

如果按以下方式修改给定的代码,Hibernate 只能发出预期的 UPDATE DML 语句。

public StateTable update(StateTable stateTable) {
    StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
    Country oldCountry = oldState.getCountry();
    // DELETE is issued, if getReference() is replaced by find().
    Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId());

    // The following line is never expected as Country is already retrieved 
    // and assigned to oldCountry above.
    // Thus, oldState.getCountry() is no longer an uninitialized proxy.
    oldState.getCountry().hashCode(); // DELETE is issued, if removed.
    stateTable.setCountry(newCountry);

    if (ObjectUtils.notEquals(newCountry, oldCountry)) {
        oldCountry.remove(oldState);
        newCountry.add(stateTable);
    }

    return entityManager.merge(stateTable);
}

在较新版本的代码中观察以下两行。

// Previously it was EntityManager#find()
Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId());
// Previously it was absent.
oldState.getCountry().hashCode();

如果最后一行不存在或 EntityManager#getReference()EntityManager#find() 替换,则会意外发出 DELETE DML 语句。

那么,这是怎么回事?特别是,我强调便携性。不跨不同的 JPA 提供程序移植这种 基本 功能会严重破坏 ORM 框架的使用。

我了解EntityManager#getReference()EntityManager#find()之间的基本区别。

一旦您的引用实体可以在其他 parent 中使用,无论如何它都会变得复杂。为了真正使其干净,ORM 必须在删除实体之前在数据库中搜索已删除实体的任何其他用途(持久垃圾收集)。这很耗时,因此不是很有用,因此没有在 Hibernate 中实现。

仅当您的 child 用于单个 parent 并且从未在其他地方重复使用时,删除孤儿才有效。在尝试重用它以更好地检测滥用此功能时,您甚至可能会遇到异常。

决定是否保留删除孤儿。如果你想保留它,你需要为新的parent创建一个新的child而不是移动它。

如果您放弃删除孤儿,您必须在 children 不再被引用时自行删除它们。

首先,让我们将您的原始代码更改为更简单的形式:

StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
Country oldCountry = oldState.getCountry();
oldState.getCountry().hashCode(); // DELETE is issued, if removed.

Country newCountry = entityManager.find(Country.class, stateTable.getCountry().getCountryId());
stateTable.setCountry(newCountry);

if (ObjectUtils.notEquals(newCountry, oldCountry)) {
    oldCountry.remove(oldState);
    newCountry.add(stateTable);
}

entityManager.merge(stateTable);

请注意,我只在第三行添加了 oldState.getCountry().hashCode()。现在您可以通过仅删除此行来重现您的问题。

在我们解释这里发生了什么之前,首先摘录一些 JPA 2.1 specification.

3.2.4:

The semantics of the flush operation, applied to an entity X are as follows:

  • If X is a managed entity, it is synchronized to the database.
    • For all entities Y referenced by a relationship from X, if the relationship to Y has been annotated with the cascade element value cascade=PERSIST or cascade=ALL, the persist operation is applied to Y

3.2.2:

The semantics of the persist operation, applied to an entity X are as follows:

  • If X is a removed entity, it becomes managed.

orphanRemoval JPA javadoc:

(Optional) Whether to apply the remove operation to entities that have been removed from the relationship and to cascade the remove operation to those entities.

我们可以看到,orphanRemoval是根据remove操作定义的,所以所有适用于remove的规则必须也申请orphanRemoval

其次,如this answer中所述,Hibernate 执行更新的顺序是实体在持久性上下文中加载的顺序。更准确地说,更新实体意味着将其当前状态(脏检查)与数据库同步并将 PERSIST 操作级联到其关联。

现在,这就是您的情况。在事务结束时,Hibernate 将持久化上下文与数据库同步。我们有两种情况:

  1. 当存在额外行 (hashCode) 时:

    1. Hibernate 与数据库同步 oldCountry。它在处理 newCountry 之前执行此操作,因为首先加载了 oldCountry(通过调用 hashCode 强制代理初始化)。
    2. Hibernate 发现 StateTable 实例已从 oldCountry 的集合中删除,因此将 StateTable 实例标记为已删除。
    3. Hibernate 与数据库同步 newCountryPERSIST 操作级联到 stateTableList,后者现在包含已删除的 StateTable 实体实例。
    4. 删除的 StateTable 实例现在再次被管理(上面引用的 JPA 规范的 3.2.2 部分)。
  2. 当没有额外的行(hashCode)时:

    1. Hibernate 与数据库同步 newCountry。它在处理 oldCountry 之前执行此操作,因为 newCountry 首先加载(使用 entityManager.find)。
    2. Hibernate 与数据库同步 oldCountry
    3. Hibernate 发现 StateTable 实例已从 oldCountry 的集合中删除,因此将 StateTable 实例标记为已删除。
    4. StateTable 实例的删除与数据库同步。

更新顺序也解释了您的发现,其中您基本上强制 oldCountry 代理初始化发生在从数据库加载 newCountry 之前。

那么,这是否符合 JPA 规范?显然是的,没有 JPA 规范规则被破坏。

为什么这不能移植?

JPA 规范(毕竟与任何其他规范一样)允许提供者自由定义规范未涵盖的许多细节。

此外,这取决于您对 'portability' 的看法。 orphanRemoval 特性和任何其他 JPA 特性在涉及其正式定义时都是可移植的。但是,这取决于您如何将它们与您的 JPA 提供程序的具体情况结合使用。

顺便说一下,规范的 2.9 部分建议(但没有明确定义)orphanRemoval:

Portable applications must otherwise not depend upon a specific order of removal, and must not reassign an entity that has been orphaned to another relationship or otherwise attempt to persist it.

但这只是规范中模糊或定义不明确的建议的一个示例,因为规范中的其他语句允许保留已删除的实体。