首先,让我们将原始代码更改为更简单的形式:
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将持久性上下文与数据库同步。我们有两种情况:
当出现多余的行(
hashCode
)时:- Hibernate
oldCountry
与数据库同步。它会在处理之前完成newCountry
,因为oldCountry
首先已加载(通过调用强制进行代理初始化hashCode
)。 - Hibernate看到
StateTable
实例已从oldCountry
的集合中删除,因此将该StateTable
实例标记为已删除。 - Hibernate
newCountry
与数据库同步。该PERSIST
操作级联到,该操作stateTableList
现在包含已删除的StateTable
实体实例。 StateTable
现在,将再次管理已删除的实例(上面引用的JPA规范的3.2.2节)。当多余的行(
hashCode
)不存在时:Hibernate
newCountry
与数据库同步。它在处理之前就完成了oldCountry
,因为newCountry
首先被加载(带有entityManager.find
)。- Hibernate
oldCountry
与数据库同步。 - Hibernate看到
StateTable
实例已从oldCountry
的集合中删除,因此将该StateTable
实例标记为已删除。 StateTable
实例的删除与数据库同步。
- Hibernate
更新的顺序还解释了您的发现,在这些发现中,您基本上
oldCountry是在
newCountry从数据库加载之前强制进行了代理初始化。
那么,这是否符合JPA规范?显然可以,没有违反JPA规范规则。
为什么这不便携?
JPA规范(毕竟像其他任何规范一样)使提供者可以自由定义规范未涵盖的许多细节。
另外,这取决于您对“可移植性”的看法。
orphanRemoval就其正式定义而言,此功能和任何其他JPA功能都是可移植的。但是,这取决于您如何结合使用它们以及JPA提供程序的详细信息。
顺便说一句,规范的 2.9 节建议(但没有明确定义)
orphanRemoval:
否则,可移植应用程序不得依赖于特定的删除顺序,并且不得将已孤立的实体重新分配给另一个关系,也不能尝试将其持久化。
但这只是规范中含糊不清或未明确定义的建议的一个示例,因为规范中的其他语句允许持久保留已删除的实体。



