Prevent Hibernate from deleting orphaned entities while merging an entity having entity associations with orphanRemoval set to true

旧巷老猫 提交于 2019-11-29 09:07:29
Dragan Bozanovic

Firstly, let's change your original code to a simpler form :

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);

Notice that I only added oldState.getCountry().hashCode() in the third line. Now you can reproduce your issue by removing this line only.

Before we explain what's going on here, first some excerpts from the JPA 2.1 specification.

Section 3.2.4:

The semantics of the flush operation, applied to an entity X are as follows:

  • If X is a managed entity, it is synchronized to the database.
    • For all entities Y referenced by a relationship from X, if the relationship to Y has been annotated with the cascade element value cascade=PERSIST or cascade=ALL, the persist operation is applied to Y

Section 3.2.2:

The semantics of the persist operation, applied to an entity X are as follows:

  • If X is a removed entity, it becomes managed.

orphanRemoval JPA javadoc:

(Optional) Whether to apply the remove operation to entities that have been removed from the relationship and to cascade the remove operation to those entities.

As we can see, orphanRemoval is defined in terms of remove operation, so all the rules that apply for remove must apply for orphanRemoval as well.

Secondly, as explained in this answer, the order of updates executed by Hibernate is the order in which entities are loaded in the persistence context. To be more precise, updating an entity means synchronizing its current state (dirty check) with the database and cascading the PERSIST operation to its associations.

Now, this is what's happening in your case. At the end of the transaction Hibernate synchronizes the persistence context with the database. We have two scenarios:

  1. When the extra line (hashCode) is present :

    1. Hibernate synchronizes oldCountry with the DB. It does it before handling newCountry, because oldCountry was loaded first (proxy initialization forced by calling hashCode).
    2. Hibernate sees that a StateTable instance has been removed from the oldCountry's collection, thus marking the StateTable instance as removed.
    3. Hibernate synchronizes newCountry with the DB. The PERSIST operation cascades to the stateTableList which now contains the removed StateTable entity instance.
    4. The removed StateTable instance is now managed again (3.2.2 section of JPA specification quoted above).
  2. When the extra line (hashCode) is absent :

    1. Hibernate synchronizes newCountry with the DB. It does it before handling oldCountry, because newCountry was loaded first (with entityManager.find).
    2. Hibernate synchronizes oldCountry with the DB.
    3. Hibernate sees that a StateTable instance has been removed from the oldCountry's collection, thus marking the StateTable instance as removed.
    4. The removal of the StateTable instance is synchronized with the database.

The order of updates also explains your findings in which you basically forced oldCountry proxy initialization to happen before loading newCountry from the DB.

So, is this according to the JPA specification? Obviously yes, no JPA spec rule is broken.

Why is this not portable?

JPA specification (like any other specification after all) gives freedom to the providers to define many details not covered by the spec.

Also, that depends on your view of the 'portability'. The orphanRemoval feature and any other JPA features are portable when it comes to their formal definitions. However, it depends on how you use them in combination with the specifics of your JPA provider.

By the way, section 2.9 of the spec recommends (but does not clearly define) for the orphanRemoval:

Portable applications must otherwise not depend upon a specific order of removal, and must not reassign an entity that has been orphaned to another relationship or otherwise attempt to persist it.

But this is just an example of vague or not-well-defined recommendations in the spec, because persisting of removed entities is allowed by other statements in the specification.

As soon as your referenced entity can be used in other parents, it gets complicated anyway. To really make it clean, the ORM had to search in the database for any other usages of the removed entity before deleting it (persistent garbage collection). This is time consuming and therefore not really useful and therefore not implemented in Hibernate.

Delete orphans only works if your child is used for a single parent and never reused somewhere else. You may even get an exception when trying to reuse it to better detect the misuse of this feature.

Decide whether you want to keep delete orphans or not. If you want to keep it, you need to create a new child for the new parent instead of moving it.

If you abandon delete orphans, you have to delete the children yourself as soon as they are not referenced anymore.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!