ManyToMany List update with detached entities throws EntityExistsException

Description

I know this is not a nice usage of Hibernate in general but we came across this issue during our Spring Boot migration (5.0.12 to 5.2.17)

Given an entity with ManyToMany list (not sure about ManyToOne ) updating the list with the same detached entities would cause an EntityExistsException.

Environment

5.2.17, Java8

Activity

Show:
Chris Cranford
November 5, 2018, 7:10 PM

, have you checked if this is a problem with Hibernate 5.3?

Bahri Gencsoy
November 6, 2018, 7:48 AM

I just did and observed that the same exception is thrown in 5.3.7 tag.

Chris Cranford
December 18, 2018, 3:12 PM
Edited

I see precisely why this happens.

The gist of the problem is a collection re-create happens where all the rows in the base table are deleted and then re-inserted with the way the collection gets reset. When this operation gets reinterpreted by Envers, it generates a series of audit operations that mimic that:

RuleName_ID *

REV *

REVTYPE

1

1

DEL

2

1

DEL

1

1

ADD

2

1

ADD

The problem here is that when Envers built the table for this collection, only the columns marked with * participate in the primary key and therefore because those 4 operations occur within the boundary of the same transaction, therefore the same revision – we face the problem where a NonUniqueObjectException gets thrown and resolved as an EntityExistsException.

The problem here is how best to resolve this.
I see 2 options forward:

1. Introduce a way to translate the above 4 changes into 2 MOD changes before save.
2. Introduce a non-backward compatible setting that controls if REVTYPE is in the primary-key.

In order to consider (1) I need to do a bit of research but the idea is to see if we can determine before save that we have this use case and consolidate those collection changes into a smaller more concise form. In short, we'd see that our collection of RuleName entities has the same entity-id with both a DEL and ADD operation. We can build a difference between the two and rewrite the collection change as a MOD instead.

In order to consider (2), we'd simply add a configuration setting that would control whether or not we add REVTYPE to the primary key of an audit table mapping that represents a collection. We'd most likely need to have this setting disabled by default in order to preserve backward compatibility and add a migration note so users know how to alter their schema to take advantage of the behavior if they wish.

As a side note I believe (2) also highlights the importance of Envers being capable of tracking what "version" of the schema it has been using and have a way to effectively migrate itself from version-to-version. By being able to do that, we'd be able to easily make rev-type a primary-key value, add a migration task and for users who upgrade – it just happens, bugfixed, and no manual migration necessary. But that's a long-term goal but we need a short-term fix now.

There are two workarounds I know of right now:

1. Don't recreate the collection
2. If you recreate the collection, do so across two transactions (clear in the first and addAll in the second).

Neither of these are likely ideal for you given your context, but at least with (2) you end up avoiding the problem. I'm not sure if either will work for your situation, but just wanted to share it with you in case.

Chris Cranford
January 8, 2019, 7:20 PM
Edited

The problem boils down to how the BasicCollectionMapper detects the difference between an ADD, a DEL, and if an element in the collection simply hasn't changed. At the moment, we do this by delegating the check to the element's Type.

There are essentially 3 types of collections we need to be concerned about:

  1. Collections of basic-types

  2. Collections of embeddables

  3. Collections of entities

For both (1) and (2), the logic the mapper uses works fine. This is because the associated Type for these two scenarios perform both a reference-equality and value-equality check.

Where the logic begins to break down is with (3) as you've noticed. The code currently delegates to EntityType but ORM only considers two entity instances to be identical if and only if they are reference-equal. In your example, the values are actually not reference equal and therefore the change delta logic results in both an ADD and DEL entry.

For (3), the audit table merely holds a reference to the owning entity id and the referenced entity id. So what I believe we can do is rather than delegate the check for (3) to the EntityType, we could instead delegate it to the entity's identifier Type which will either be a basic-type or a composite-type and performs the same type of check used for (1) or (2) depending on if the identifier is basic or an embeddable.

For Envers 5.x, I believe this is a decent compromise. This allows us to fix the underlying problem in such a way that we do not need to impose any schema alterations for users mid major release to deal with the the fact that REVTYPE is not part of the collection table's primary key.

For Envers 6, I'll enforce REVTYPE being part of the primary-key with a backward compatibility configuration option that can be enabled so that existing applications can continue to support legacy behavior until they can manually modify their schema accordingly.

Assignee

Chris Cranford

Reporter

Bahri Gencsoy

Fix versions

Labels

None

backPortable

None

Suitable for new contributors

None

Requires Release Note

None

Pull Request

None

backportDecision

None

Worked in

5.0.12

Components

Affects versions

Priority

Minor
Configure