22

举一个非常简单的一对多关系(国家/地区->)示例。

国家(反面):

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

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

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

EclipseLink 正是完成了这项工作。它在给定的场景中发出一条语句(与set toUPDATE具有相同的关系)。orphanRemovaltrue

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


这只是试图使双方的双向关系更加一致。

如有必要,上述代码段中使用的防御性链接管理方法在实体中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()

4

2 回答 2

12

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

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,如果与 Y 的关系已使用级联元素值 cascade=PERSIST 或 cascade=ALL 进行注释,则将持久操作应用于 Y

3.2.2节:

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

  • 如果 X 是一个被移除的实体,它就会变成托管的。

orphanRemovalJPA javadoc

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

可以看到,orphanRemoval是按照remove操作来定义的,所以所有申请的规则也remove 必须申请orphanRemoval

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

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

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

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

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

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

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

为什么这不是便携式的?

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

此外,这取决于您对“便携性”的看法。该orphanRemoval功能和任何其他 JPA 功能在其正式定义方面都是可移植的。但是,这取决于您如何结合 JPA 提供者的具体情况使用它们。

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

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

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

于 2015-12-12T14:27:09.987 回答
4

只要您引用的实体可以在其他父母中使用,它就会变得复杂。为了真正让它变得干净,ORM 必须在数据库中搜索已删除实体的任何其他用途,然后再将其删除(持久垃圾收集)。这是耗时的,因此不是真正有用的,因此没有在 Hibernate 中实现。

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

决定是否要保留删除孤儿。如果要保留它,则需要为新父级创建一个新子级,而不是移动它。

如果您放弃删除孤儿,则必须在不再引用子项时自己删除它们。

于 2015-12-11T07:44:40.360 回答