1

I have an extensive JPA model which exclusively uses unidirectional @ManyToOne relationships from child to parent, for example like this.

public class Child {
  @ManyToOne(optional = false)
  @JoinColumn(name = "parent")
  private Parent parent
}

There are zillions of relations like this in a rather convoluted hierarchy leading to a root-most entity. We've set up database cascading deletes, so it is only necessary to delete the root-most entity when we want to delete things. This was considerably more efficient than doing it in JPA. This used to work fine but with Hibernate 6 we're encountering a problem.

If I've loaded an Child entity at some point in a test, say, then try to delete a Parent entity I get an exception like this

org.hibernate.TransientObjectException: persistent instance references an unsaved transient instance of 'Parent'

I can see from the stack that this exception is being raised via CascadingAction.CHECK_ON_FLUSH. From debug I can also see that the relation triggering this is from child to parent. This is perhaps not unreasonably checking that the loaded entities don't have any dangling references. They obviously will, however, because we're not deleting things using JPA, it is done via data base cascading deletes.

I really don't want to have to replicate the DB cascading deletes in JPA and as mention it is considerably slower than data base cascading deletes.

Is there a way to disable this check, or to instead flag these relationships as data-base cascading deletes so Hibernate knows it can just remove the entities safely?

NB I'm using Spring Boot.

12
  • "We've set up database cascading deletes" - as in, directly in the database DDL and no Hibernate/JPA cascaded deletes? Commented Nov 6 at 12:51
  • Yeah the cascades are all defined in the database schema. Commented Nov 6 at 13:26
  • 1
    Be a man, switch to JDBC. Commented Nov 6 at 14:32
  • 1
    You have to evict all loaded children manually. Otherwise even if you suppress the error Hibernate may try to save them. Commented Nov 6 at 15:31
  • 2
    Hibernate 6 has seen many breaking changes ever since hibernate-entitymanager was replaced with built-in JPA support that is far more compliant to the JPA spec. This may have been a situation of "it should have always been broken, you were lucky until now". Data disappearing outside of the view of HIbernate is pretty much automated chaos. That is the thing with Hibernate/JPA... if you want to use it, use it. Don't half-use it and then go do low level SQL anyway. Use JPA cascades, not DDL cascades. If you want to use DDL cascades, you should look into using something like JOOQ instead. Commented Nov 6 at 15:40

2 Answers 2

1

We've set up database cascading deletes [...]. This was considerably more efficient than doing it in JPA.

I can believe that it provided faster deletion behavior, but it was never a good idea. JPA and other ORM systems are designed and built for state management, not merely query building. Undercutting their ability to track the state they are entrusted with managing cannot reasonably be expected to end well.

This used to work fine but with Hibernate 6 we're encountering a problem.

I am prepared to believe that you did not recognize any problems with the database-level cascading deletes in conjunction with older versions of Hibernate. It might even have been that there genuinely weren't any. Nevertheless, see "cannot reasonably be expected to end well", above. Even if hadn't before, that misfortunate ending has arrived now.

If I've loaded an Child entity at some point in a test, say, then try to delete a Parent entity I get an exception

I presume you mean that you delete the particular Parent with which the Child in question is associated. The resulting DB-level cascading deletes then yank the persistent representation of the Child out from underneath the persistence unit. The persistence unit is thereby made inconsistent: the state of the Child as judged by the database is deleted / transient, but it is still persistent / managed as far as the entity manager is concerned. The best you can hope for is that the EM doesn't notice and / or doesn't care, but there is not, in general, any basis for assuming that that's what you will actually observe.

I really don't want to have to replicate the DB cascading deletes in JPA and as mention it is considerably slower than data base cascading deletes.

If Hibernate is to maintain synchronicity with the DB then there is more work to do than just removing rows from the DB. Yes, that will be more costly than allowing Hibernate to lose sync, but losing sync was never a very good option. However, you might get some relief by adding a (Hibernate-specific) @OnDelete annotation:

public class Child {
  @ManyToOne(optional = false)
  @JoinColumn(name = "parent")
  @OnDelete(action = CASCADE)
  private Parent parent
}

This tells Hibernate that there should be an on delete cascade clause on the foreign key constraint supporting the relationship. As I understand the docs, however, you would still need to use that together with JPA-level cascading removes in order for it to be effective in preventing the persistence context from losing synchronization with the DB. And I think that means you'll need to make the relationship bidirectional, though one or both ends can be lazy-loaded. Hibernate will fetch the relationship if necessary so that it knows what entities need to be evicted from its cache, but with @OnDelete, is should not execute delete statements on the database for those.

Is there a way to disable this check,

Not as far as I can tell.

or to instead flag these relationships as data-base cascading deletes

See above, but, as already described, this ...

so Hibernate knows it can just remove the entities safely?

... does not follow. Hibernate needs to know what child (and grandchild, etc) entities are deleted in order to manage its cache appropriately. That will have a cost, both in code to map the other side of the relationship and in DB operations performed, but it should be at least a bit cheaper than without the @OnDelete.

Sign up to request clarification or add additional context in comments.

2 Comments

I tried various annotations on the @ManyToOne end but they all seem to control propagation from Child to Parent whereas the problem I'm having is the propagation from Parent to Child and as mentioned these associations are all unidirectional right now.
Yes, the cascade attribute of a relationship annotation controls which operations cascade from the entity on which the annotation appears to the one on the other side of the relationship. I did say " I think that means you'll need to make the relationship bidirectional".
0

As per the comments the problem is that if the database cascades the deletes then the persistence context is left in an inconsistent state. This used to be ignored but now Hibernate detects the inconsistency and raises an error.

It is therefore necessary to either replicate all the cascading deletes in JPA (which is not DRY so I'm not doing that) or clear the persistence context. Clearing the persistence context on delete can be achieved like this.

@NoRepositoryBean
public interface CascadingDeleteRepository<TEntity, TId> extends CrudRepository<TEntity, TId> {

  @Override
  default void delete(TEntity entity) {
    cascadingDelete(entity);
  }

  @Query("DELETE FROM #{#entityName} e WHERE e = :entity")
  @Modifying(clearAutomatically = true)
  void cascadingDelete(@Param("entity") TEntity entity);
}

In an ideal world, perhaps, there would be a way to tell JPA that an association is cascaded on delete by the DB, and then the persistence context could automatically clean up transitive references to the deleted entity.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.