一尘不染

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

hibernate

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

国家(反面):

@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;

尝试更新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);
}

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

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

尽管orphanRemoval设置为true,但我希望Hibernate发出常规的UPDATEDML语句,导致orphanRemoval完全暂停其作用,因为关系链接已迁移(而不是简单删除)。

EclipseLink正是完成了这项工作。它UPDATE在给定的场景中发出一条语句(与orphanRemoval设置为具有相同的关系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);
    }
}

更新:

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);
}

在新版本的代码中,请注意以下两行。

// 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()


阅读 388

收藏
2020-06-20

共1个答案

一尘不染

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

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规范的
一些摘录。

3.2.4 节:

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

  • 如果X是受管实体,则将其同步到数据库。
    • 对于由X的关系引用的所有实体Y,如果已使用级联元素值Cascade = PERSIST或Cascade =
      ALL注释了与Y的关系,则对Y应用持久性操作

3.2.2 节:

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

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

orphanRemovalJPA
javadoc

(可选)是否 将删除操作 应用于已从关系中删除的实体,以及是否将删除操作应用于这些实体。

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

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

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

  1. 当出现多余的行(hashCode)时:

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

    6. Hibernate newCountry与数据库同步。它在处理之前就完成了oldCountry,因为newCountry首先被加载(带有entityManager.find)。

    7. Hibernate oldCountry与数据库同步。
    8. Hibernate看到StateTable实例已从oldCountry的集合中删除,因此将该StateTable实例标记为已删除。
    9. StateTable实例的删除与数据库同步。

更新的顺序还解释了您的发现,在这些发现中,您基本上oldCountry是在newCountry从数据库加载之前强制进行了代理初始化。

那么,这是否符合JPA规范?显然可以,没有违反JPA规范规则。

为什么这不便携?

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

另外,这取决于您对“可移植性”的看法。orphanRemoval就其正式定义而言,此功能和任何其他JPA功能都是可移植的。但是,这取决于您如何结合使用它们以及JPA提供程序的详细信息。

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

否则,可移植应用程序不得依赖于特定的删除顺序,并且不得将已孤立的实体重新分配给另一个关系,也不能尝试将其持久化。

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

2020-06-20