Potential synchronization issue with orphanRemoval=true and query auto-flush?

Hello everyone,

I recently encountered what I think is a very surprising behavior with orphanRemoval=true and auto-flush.

Some context

An integration test failed after I tried to change the @Id @GeneratedValue of an Event entity from UUID to IDENTITY. For some reason, an entity Projection had a collection with 12 children instead of just the expected 3.

What we were doing in the transaction is some basic “event-sourcing” stuff:

  • persist a new Event entity
  • request the database with a HQL query for a bunch of Event entities, including the new one.
    • I’ll refer to this step with (A) later.
  • process these events to create an aggregate view.
  • update an existing Projection entity using this aggregate view as the source.
    • This is where the orphanRemoval=true collection is cleared and replaced with new child entities.

Now, in my integration test there were 4 events to be persisted in the same transaction, so the steps from above were repeated for each event.

I initially thought the change to @GeneratedValue(strategy = IDENTITY) caused this to happen because of the early-insert behavior of the ActionQueue which maybe could have caused some child entities to be flushed earlier than before. Actually, this early-insert led to an optimization of the (A) query to not trigger an auto-flush anymore.

The test case

Please find here a minimal reproducer test case.

I dug around the codebase a little and I think what happens in the for-loop starting at l.40 is:

  • second loop : the HQL query triggers a preFlush event, which eventually goes into Cascade#cascadeCollectionElements
    • two insertion events are added to the ActionQueue (one for each ChildEntity of the 1st loop)
    • two deletion events are added to the queue via deleteOrphans (one for each child created in the first transaction)
  • Hibernate determines that a flush isn’t actually needed because the query space is not related to the events in the ActionQueue.
  • third loop : the HQL query triggers a preFlush event
    • two insertion events are added to the ActionQueue (one for each ChildEntity of 2nd loop)
    • here’s where it gets interesting : no deletion events are added to the queue because deleteOrphans get its orphans from the collection snapshot… which is only updated after a flush! So, the “snapshot orphans” still refer to the children from the first transaction, even though they’ve already been marked for removal by the previous loop.
  • When the transaction commits, the ActionQueue has 6 pending insertions and 2 pending deletions to be flushed.
    • Depending on the @JoinColumn attributes, the parent may have 2 or 6 children. However, in every case, there are 6 ChildEntity in the database.
    • Side note: this also happens if the @OneToMany side owns the relationship.

So I would be very interested to hear about your opinions on this.

I think this is a very counter-intuitive behavior, perhaps even a bug ? I was quite surprised to see that a seemingly innocuous query on some unrelated entities could disrupt the synchronization of an orphanRemoval collection and its children.

Cheers,

Maxime

Sounds like a bug to me. Please create a bug ticket in our issue tracker and attach that reproducer.

Thank you for your reply, I created the ticket HHH-19942.

1 Like