orphanRemoval 不适用于通过 SpringData .save 方法保存的 naturalId 实体
更新/TLDR:合并到持久性上下文中的实体无法正确处理用 orphanRemoval 注释的集合。
简约的例子在这里: https://github.com/alfonz19/orphan-removal -test/tree/justMergeFlow
详细信息请参阅 README.md。
原始帖子:
Spring 有这个 save 方法
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
,它带来了一些方便的行为,但它似乎会导致令人惊讶的事情,因为如果您最初通过持久、合并或保存创建实体,孤儿删除似乎会受到影响。如果您可以对此发表评论,请发表评论。我还没有最小的例子,但理论上我可以创建它。
讨论的场景:
- 您从空数据库开始,您有微不足道的 1:N 关联。它只是将一些字符串映射到实体,就像您想避免使用逗号分隔的列表一样。
关联的非拥有方(1:
),具有naturalID,关联注释为:
@OneToMany(mappedBy = "xxx", cascade = CascadeType.ALL, orphanRemoval = true)
关联的拥有方注释如下,实体具有复合自然pk:
@EmbeddedId
private PK pk;
@ManyToOne
@JoinColumn(name = "xxx", insertable = false, updatable = false)
@Setter(AccessLevel.NONE)
private XXX xxx;
- 处理。单个事务,您在新状态下创建实体,将一个关联实体添加到列表中并保存该实体(我们稍后将回到保存操作)。现在我们从集合中删除该项目(如果给定项目/清除/其他)和另一个项目。 TX 提交。
此后数据库中将会有什么?嗯,我有 3 个不同的结果
:如果初始保存操作是entityManager.persist,结果正如我所期望的:顶级记录存在并且具有单个关联项,即我们没有在其关联列表中删除的项。那太棒了。
b.如果初始save操作是entityManager.merge或SimpleJpaRepository#save(由于naturalId实体的isNew行为,它将调用合并;id != null --> 据称它已分离),该实体将也被创建,但是列表上的修改不会被持久化,这意味着即使我可以看到,在提交之前实体的目标状态是变体a)中描述的状态,我们从关联中删除的项目将被持久化并且另一个则不然。 IE。我们调用保存的状态将被插入,关联列表上的进一步更改将不会反映出来。
c.如果没有离奇的案例,每一个这样的问题都是不完整的,我很高兴我不会让你失望。当所有这些之间有更多操作,但没有接触这些实体时,可能会引发一些刷新,最后我在关联集合中留下了两个实体;这意味着顶级实体必须已被管理并反映了对其关联实体的更改,但不知何故,休眠不需要感受到删除实体的压力,该实体已从该集合中删除。
解决方案(或者可能是解决方法)很简单:如果您知道自己正在坚持,则只需使用坚持即可。简单的。然后它就会正常工作而不会出现问题。我认为这甚至是正确的举动,我个人不喜欢 SimpleJpaRepository#save 背后的想法。但我觉得我可能会遗漏一些东西,因为编写 save 方法似乎很危险,如果这是可能的结果,它将持续或合并。我绝对不明白场景 b/c 的原因。即使我在这里错误地使用了合并,实体在被带入持久性上下文后也会被管理,并且应该正确处理其关联集合,但事实并非如此。
注意:
- 是的,一切都是在一次交易中完成的。
- 我检查了每个实体是否以及何时处于持久化上下文中,并且我没有发现用作 save 方法场景的持久/合并之间有任何区别。所以我不知道为什么会有不同的结果。
知道我还可以检查什么吗?或者甚至我的错误在哪里?
UPDATE/TLDR: entity merged into persistence context does not correctly handle collection annotated with orphanRemoval.
minimalistic example is here: https://github.com/alfonz19/orphan-removal-test/tree/justMergeFlow
please see README.md for details.
ORIGINAL POST:
Spring has this save method
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
which brings some convenient behavior, but it seems it can lead to surprising things, as orphan removal seems to be affected, if you initially created the entity via persist, merge or save. If you can comment on it, please do. I don't have minimal example yet, but I can theoretically create it.
discussed scenario:
- you start with empty db, you have trivial 1:N association. It's just mapping some strings to entity, like if you'd like to avoid comma separated list.
non-owning side of association(the 1:
), has naturalID, and association is annotated as:
@OneToMany(mappedBy = "xxx", cascade = CascadeType.ALL, orphanRemoval = true)
owning side of association is annotated as following, and entity has composite natural pk:
@EmbeddedId
private PK pk;
@ManyToOne
@JoinColumn(name = "xxx", insertable = false, updatable = false)
@Setter(AccessLevel.NONE)
private XXX xxx;
- processing. Single transaction, you create entity in new state, add one association entity into list and save the entity (we will get back to save operation later). Now we remove that item from collection (removeIf given item / clear / whatever) and another one. TX commit.
Now what will be in the database after this? Well, I have 3 different results:
a. If the initial save operation was entityManager.persist, the result is as I'd expect: top-level record exist and has single associated item, the one we didn't remove in its association list. That's great.
b. If the initial save operation was entityManager.merge or SimpleJpaRepository#save (which will call merge, because of isNew behavior for naturalId entity; id != null --> it's detached allegedly), the entity will be also created, but modifications on list won't be persisted, meaning even though I can see, that before commit the target state of entity is the one described in variant a), the item which we removed from association will be persisted and the other one not. Ie. the state with which we called save will be inserted, further alterations on association list won't be reflected.
c. and every such issue won't be complete without bizarre case, and I'm glad I need not to disappoint you. When there are more operation in between all of this, but which are not touching these entities, probably some flush is induced, and in the end I'm left with both entities in association collection; meaning that the top-level entity must have been managed and changes to its association entities reflected, but somehow hibernate need not felt pressure to remove entity, which was removed from that collection.
The solution(or maybe workaround) is simple: just use persist if you know that you're persisting. Easy. Then it will just work without hiccup. And I think it's even correct move, personally I don't like the idea behind SimpleJpaRepository#save. But I feel I might be missing something, as this seems to be to dangerous to write save method, which will do persist or merge, if this is possible outcome. And I definitely don't see the reason for scenario b/c. Even if I use merge incorrectly here, the entity is managed after being brought to persistence context, and touching its association collection should be correctly handled, yet it's not.
Notes:
- Yes, everything is done in single tx.
- I checked whether every entity is and when in persistence context, and I didn't find any difference between persist/merge used as save method scenario. So I have no idea why there is different outcome.
Any idea what could I check more? Or even where my mistake is?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
这被确定为 hibernate 中的一个错误,它已经被修复,修复应该是 6.0.0 版本的一部分。欲了解更多信息:
https://hibernate.atlassian.net/browse/HHH-15098
This was identified as a bug in hibernate, it was fixed already, fixed should be part of 6.0.0 version. For more info:
https://hibernate.atlassian.net/browse/HHH-15098