Merge entity with @OneToMany attributes leading to javax.persistence.EntityNotFoundException: No row with the given identifier exists

Hello,

during a session.merge() I’m getting a javax.persistence.EntityNotFoundException: No row with the given identifier exists Exception.

The business process is as follows:

  • Client receives an object with it’s dependencies loaded by the server
  • Client changes the dependencies (OneToMany) and sends it to the server
  • Server tries to merge

I added the source code showing the problem here (~5kb)
https://www.magentacloud.de/lnk/MfmYjJOQ

The doWork method leads to a unique constraint exception due to a item_id and language being a unique constraint and Hibernate doing the inserts before the deletes on flush.

The doWork2 method leads to the aforementioned javax.persistence.EntityNotFoundException: No row with the given identifier exists.

One way I found to fix the problem in doWork2 is to remove the snapshots of the PersistantSet doing ((PersistentSet) item.getLabel()).setSnapshot(null, null, null);

The questions I have are now as follows:

  1. Is Hibernate meant to do the inserts before the deletes in method doWork ?
  2. Will removing the snapshots on the PersistantSet have any strange sideeffects?
  3. Is there a better way to implement the given business process?

Note: We recently upgraded Hibernate from 3.6.4 to 5.2.12 and I think the exceptions didn’t occure back in the old version

Hope you can help me out, kind regards
René

Is Hibernate meant to do the inserts before the deletes in method doWork?

Yes. To preserve the order of operations the insert is done before delete as explained in this article. If you bump into such issue, it means you were doing the merge like this:

  • you try to clear the collection
  • then you re-add back the entries sent from the client

That’s not a good approach. You should do the merging from the incoming collection and the one in the database like this:

  • add the new entries
  • update the remaining ones
  • delete the ones that are no longer found in the incoming collection

For more details, check out this article.

Will removing the snapshots on the PersistantSet have any strange sideeffects?

Better do what I advised you already instead of trying to modify Hibernate internals.

Is there a better way to implement the given business process?

Yes, as already mentioned.

We recently upgraded Hibernate from 3.6.4 to 5.2.12 and I think the exceptions didn’t occure back in the old version

Hibernate 3.6 is 7 years old now. The flush operation order hasn’t changed for a very long time so this issue might be replicating on 3.6 as well. On the other hand, there are many performance improvements in 5.x that you will not find in 3.6.

Hello,
thanks for the quick reply.
Unfortunately I’m not really able to fix my issue due to the unique constraint in my example and hibernate/jpa doing inserts before deletes.

Maybe I’m misunderstanding your suggestion, especially this part

delete the ones that are no longer found in the incoming collection

here’s what I did:
On the client side I added the new entries and removed the unused ones (which happen to be all of the old ones).
On the server side I just merge and flush, with the flush ending in the unique constraint exception.

Is there some way to force hibernate to do the delete first in this case? A flush before the merge doesn’t do anything obviously, due to the Hibernate not knowing about the client changes yet.

Hope you can help me out, this issue is driving me a bit nuts.

That’s where you go wrong. If you have a unique key, then you should locate the entity by that unique key and update it.

Hibernate provides the @NaturalId annotation which is backed by a unique key, In your case, the @NaturalId is exactly the column that throws the ConstraintViolationException.

So, the client sends you the new state. Instead of just calling merge on the entity, you have to:

1.Fetch the database entity along with its children
2. Compare the incoming state from the client with the one you fetched.
3. You match child entities by their @Id or by their @NaturalId. If there’s a match, then you should just update the child entity.
4. If you can’t find the child entity sent by the client, it means it’s a new child entity so you have to add it to the collection in the managed entity you fetched from the DB.
5. If there’s a child entity in the collection from the DB which can’t be found in the collection from the client, it means you can simply remove it from the collection in the managed entity (you fetched from the DB).

This is the right way to do it, and it also generates the right number of SQL statements. Deleting all entries and re-adding them back is not efficient since it unbalances and rebalances the associated B+Tree indexes.

Is there some way to force hibernate to do the delete first in this case?

Even if you can do it, it’s not efficient. You could just load the entity from the DB, clear the entity collection, flush, and re-add the child entities coming from the client. However, this is not the proper way to do it.

Hey,
thank’s for the detailed explanation. I managed to do it the way you described it :slight_smile:

You are very welcome.

Whenever I try removing from PersistentSet, the item is still in the collection. Can you maybe point out why this might be the case?

That is the reason why I went with the OP’s original idea of refreshing all entities. For all the entities I had to implement Persistable to manage their isNew, as well as implement custom repositories to manage all the children, to properly overwrite them and their descendants. And do it recursively using children entities’ custom repositories. And now I’m stuck at the same issue as OP

javax.persistence.EntityNotFoundException: Unable to find project.story.model.StoryPlayCriteriaStateRequirement with id StoryPlayCriteriaStateRequirement.CompositeKey(stateId=deadbeef-0000-5773-0000-000000008003, storyPlayCriteria=StoryPlayCriteria(id=deadc0de-0000-5727-fdbc-c00000000000, version=0, isNew=true, stories=[Story(id=deadc0de-0000-5727-fdbc-000000000000, version=0, isNew=false)]))