Issue
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?
Solution
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
Answered By - Martin Mucha
Answer Checked By - Clifford M. (JavaFixing Volunteer)