举一个非常简单的一对多关系(国家->
州(的例子。
国家(反面( :
@OneToMany(mappedBy = "country", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<StateTable> stateTableList=new ArrayList<StateTable>(0);
状态表(拥有方(:
@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 - 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
实体由客户端应用程序提供,该应用程序有兴趣将实体关联Country
(countryId = 67
StateTable
(更改为其他内容(countryId = 68
((因此在JPA中相反,将子实体从其父实体(集合(迁移到另一个父实体(集合(,而orphanRemoval=true
又会反对(。
Hibernate提供程序发出DELETE
DML语句,导致与StateTable
实体对应的行从基础数据库表中删除。
尽管orphanRemoval
设置为true
,但我预计Hibernate会发出一个常规的UPDATE
DML声明,导致orphanRemoval
的效果完全暂停,因为关系链接被迁移(而不是简单地删除(。
EclipseLink 正是完成这项工作。它在给定的场景中发出UPDATE
语句(与设置为 true
的orphanRemoval
具有相同的关系(。
哪一个的行为符合规范?在这种情况下,除了从反向删除orphanRemoval
之外,是否可以使 Hibernate 发出UPDATE
语句?
这只是试图使双方的双向关系更加一致。
如有必要,上述代码片段中使用的防御性链接管理方法即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);
}
}
更新:
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()
之间的基本区别.
首先,让我们将您的原始代码更改为更简单的形式:
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 的关系已使用级联元素值进行批注 级联=持久或级联=全部,持久操作应用于 Y
第 3.2.2 节:
应用于实体 X 的持久操作的语义如下 遵循:
- 如果 X 是已删除的实体,则它将成为托管实体。
orphanRemoval
JPA javadoc:
(可选(是否将删除操作应用于以下实体: 已从关系中删除并级联删除 对这些实体的操作。
如我们所见,orphanRemoval
是根据remove
操作定义的,因此适用于remove
的所有规则也必须适用于orphanRemoval
。
其次,如本答案所述,Hibernate执行的更新顺序是实体在持久性上下文中加载的顺序。更准确地说,更新实体意味着将其当前状态(脏检查(与数据库同步,并将PERSIST
操作级联到其关联。
现在,这就是你的情况。在事务结束时,Hibernate将持久性上下文与数据库同步。我们有两种情况:
当存在额外的线路(
hashCode
(时:- 休眠与数据库同步
oldCountry
。它在处理newCountry
之前执行此操作,因为首先加载了oldCountry
(通过调用hashCode
强制代理初始化(。 - Hibernate看到一个
StateTable
实例已从oldCountry
的集合中删除,从而将StateTable
实例标记为已删除。 - 休眠
newCountry
与数据库同步。PERSIST
操作级联到现在包含已删除StateTable
实体实例的stateTableList
。 - 现在将再次管理已移除的
StateTable
实例(上面引用的 JPA 规范的 3.2.2 部分(。
- 休眠与数据库同步
当额外的行(
hashCode
(不存在时:- 休眠将
newCountry
与数据库同步。它在处理oldCountry
之前执行此操作,因为newCountry
首先加载(带有entityManager.find
(。 - 休眠与数据库同步
oldCountry
。 - Hibernate看到一个
StateTable
实例已从oldCountry
的集合中删除,从而将StateTable
实例标记为已删除。 - 删除
StateTable
实例与数据库同步。
- 休眠将
更新的顺序还解释了您的发现,在这些发现中,您基本上强制在从数据库加载newCountry
之前进行代理初始化oldCountry
。
那么,这是根据 JPA 规范吗?显然是的,没有违反 JPA 规范规则。
为什么这不是便携式的?
JPA 规范(毕竟像任何其他规范一样(让提供者可以自由地定义规范未涵盖的许多细节。
此外,这取决于您对"可移植性"的看法。orphanRemoval
功能部件和任何其他 JPA 特性在正式定义方面都是可移植的。但是,这取决于您如何结合 JPA 提供程序的具体情况使用它们。
顺便说一下,规范的第 2.9 节建议(但没有明确定义(orphanRemoval
:
否则,便携式应用程序不得依赖于特定订单 删除,并且不得将孤立的实体重新分配给 另一种关系或以其他方式尝试保持它。
但这只是规范中模糊或定义不明确的建议的一个示例,因为规范中的其他语句允许保留已删除的实体。
一旦您引用的实体可以在其他父级中使用,它就会变得复杂。为了真正使其干净,ORM必须在删除之前在数据库中搜索已删除实体的任何其他用法(持久垃圾收集(。这很耗时,因此不是真正有用,因此无法在 Hibernate 中实现。
删除孤立项仅在您的孩子用于单亲父母并且从未在其他地方重复使用时才有效。在尝试重用此功能以更好地检测此功能的滥用时,您甚至可能会收到异常。
决定是否要保留删除孤立项。如果要保留它,则需要为新父项创建一个新子项,而不是移动它。
如果您放弃删除孤立项,则必须在不再引用子项时立即自行删除它们。