13

I have these entities

class Foo{
    Set<Bar> bars;
}

class Bar{
    Foo parent;
    String localIdentifier;
}

With this mapping (sorry, no annotations, I'm old fashioned):

<class name="Foo">
    ...
    <set name="bars" cascade="all-delete-orphan" lazy="false" inverse="true">
        <key>...</key>
        <one-to-many class="Bar"/>
    </set>
</class>


<class name="Bar">
    ...
    <property name="localIdentifier" column="local_identifier"/>
    <many-to-one name="parent" column="parent_id" />
</class>

I also have a unique constraint on 2 columns: local_identifier and parent_id (not a unique constrain on each, but a single unique constrain containing both, e.g. no 2 rows with the same parent and same localIdentifier are allowed)

alter table bar add constraint unique_bar unique (parent_id, local_identifier)

And this code that uses them:

//foo is persistent, foo id = 1
Bars bars = foo.getBars();
bars.clear(); // bars contained 1 item [parent_id = 1, local_identifier = "a"]
Bar newBar = new Bar();
newBar.setParent(foo);
newBar.setLocalIdentifier("a");
bars.add(newBar);

Now, for some reason, Hibernate doesn't execute things in the order they were called. It doesn't execute the clear() (delete) before the add() (insert) but vice versa, it first tries to insert, getting a ConstraintViolationException

I know adding a little session.flush() after bars.clear(); , could fix this, but in this case, I have no access to the session in a non ugly way.

So is flush is the only solution? or is there a Hibernate version that respects the order of actions?

Update: By the way, dereferencing the collection will result in a HibernateException from https://www.hibernate.org/117.html#A3:

I get HibernateException: Don't dereference a collection with cascade="all-delete-orphan" This will happen if you load an object with a cascade="all-delete-orphan" collection and then remove the reference to the collection. Don't replace this collection, use clear() so the orphan-deletion algorithm can detect your change.

Eran Medan
  • 44,555
  • 61
  • 184
  • 276

4 Answers4

8

I guess there is no alternative to flushing

From here:

Hibernate is violating a unique constraint!

Hibernate isn't quite as clever with unique constraints as it is with foreign keys. Sometimes you might need to give a little hint.

A unique constraint violation could occur if two objects are both being updated, one is "releasing" a value and the other is "obtaining" the same value. A workaround is to flush() the session manually after updating the first object and before updating the second.

(This kind of problem occurs rarely in practice.)
christopheml
  • 2,444
  • 17
  • 25
Eran Medan
  • 44,555
  • 61
  • 184
  • 276
1

If you want to avoid flushing the session here, try to replace the whole list (new List<Bar>() instead of Clear()). Hibernate should actually remove all the items in one shot before adding new. Just a try, not sure if it works.

Stefan Steinegger
  • 63,782
  • 15
  • 129
  • 193
1

If you are using oracle, you could also use deferrable constraints to postpone the checking of the constraints until the transaction is committed. Not sure if/how this is supported by other databases.

Guy Mahieu
  • 11
  • 3
0

I had a slightly similar issue, where I had a unique index on the child table on certain condition. So my issue was when I remove a record that satisfied this condition, then add another record that will restore that condition back, I got the UNIQUE INDEX issue.

After going through many trials and suggestions. I didn't like the idea of flushing after deleting and before the adding, I assume it will work though, I went to a constraint solution and dropped the unique index (as I didn't really needed the columns to be indexed) which did work:

(On postgres database)

ALTER TABLE bar ADD CONSTRAINT bar_uc
        EXCLUDE (parent_id WITH =) WHERE (condition = true) INITIALLY DEFERRED;

INITIALLY DEFERRED is the key here.

Sample code that I was using:

//Transaction 1
Foo parent = new Foo('some-id');
boolean condition = true;
Bar c = new Bar('some-id', parent, condition);
parent.getChildren().add(c);
fooService.save(parent);    //children list are cascaded

... later on

//Transaction 2
boolean condition = true;
Foo parent = fooSerice.findById('some-id');    //children list is eagerly fetched
Bar c1 = parent.getChildren().stream().filter(c -> c.getId().equals("some-id")).findFirst().get();
Bar c2 = new Bar('some-id1', parent, condition);
parent.getChildren().remove(c1);
parent.getChildren().add(c2);