阻止Hibernate删除孤立实体,同时将具有实体关联的实体与orphanRemoval设置为true

Tin*_*iny 22 hibernate jpa orphaned-objects orphan

以一对多关系(国家/地区->)为例.

国家(反面):

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

StateTable(拥有方):

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

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

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 - http://stackoverflow.com/a/31761967/1391249)
    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);
}
Run Code Online (Sandbox Code Playgroud)

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

Hibernate提供程序发出DELETEDML语句,导致StateTable从基础数据库表中删除与该实体对应的行.

尽管orphanRemoval已设置为true,但我希望Hibernate发出一个常规UPDATEDML语句,导致orphanRemoval其完全挂起的效果,因为关系链接已迁移(不是简单地删除).

EclipseLink完成了这项工作.它UPDATE在给定的场景中发出声明(与orphanRemovalset 具有相同的关系true).

哪一个符合规范?UPDATE在这种情况下,除了orphanRemoval从反面移除之外,是否可以使Hibernate发出声明?


这只是试图使双向关系在双方都更加一致.

防守链接管理方法即add()remove()在上述代码段,用于如果需要,在所定义的Country实体如下.

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);
    }
}
Run Code Online (Sandbox Code Playgroud)


更新:

UPDATE如果给定的代码按以下方式修改,则Hibernate只能发出预期的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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

那么,这里发生了什么?特别是,我强调可移植性.不在不同的JPA提供商之间移植这种基本功能会严重破坏ORM框架的使用.

我理解EntityManager#getReference()和之间的基本区别EntityManager#find().

Dra*_*vic 11

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

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);
Run Code Online (Sandbox Code Playgroud)

请注意,我只oldState.getCountry().hashCode()在第三行添加了.现在,您只需删除此行即可重现您的问题.

在我们解释这里发生了什么之前,首先从JPA 2.1规范中摘录一些内容.

3.2.4节:

应用于实体X的刷新操作的语义如下:

  • 如果X是托管实体,则会将其同步到数据库.
    • 对于来自X的关系引用的所有实体Y,如果与Y的关系已使用级联元素值cascade = PERSIST或cascade = ALL进行注释,则将持久化操作应用于Y

3.2.2节:

应用于实体X的持久化操作的语义如下:

  • 如果X是已删除的实体,则它将被管理.

orphanRemovalJPA javadoc:

(可选)是否将删除操作应用于已从关系中删除的实体,并将删除操作级联到这些实体.

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

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

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

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

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

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

更新顺序还说明了您在从数据库oldCountry加载之前基本上强制进行代理初始化的结果newCountry.

那么,这是根据JPA规范吗?显然是的,没有违反JPA规范规则.

为什么这不便携?

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

此外,这取决于您对"便携性"的看法.该orphanRemoval功能和任何其他JPA功能在其正式定义时都是可移植的.但是,这取决于您如何结合JPA提供程序的细节使用它们.

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

否则,便携式应用程序必须不依赖于特定的删除顺序,并且不得将已经孤立的实体重新分配给另一个关系或以其他方式尝试保留它.

但这仅仅是规范中模糊或未明确定义的建议的一个示例,因为规范中的其他语句允许持久删除实体.