TransientObjectException when mixing EntityManager.delete with delete statements in 6.6.x

Hi all,

Since the upgrade to WildFly 34, with Hibernate 6.6.1, we are seeing various TransientObjectException like org.hibernate.TransientObjectException: persistent instance references an unsaved transient instance of 'org.hibernate.entities.OtherEntity' (save the transient instance before flushing). This issue was already reported by another user and several bugs were opened on Jira: HHH-18501, HHH-18614, HHH-18622, HHH-18751 and HHH-18814. All these bugs were closed with the comment that this is not a bug in Hibernate, but rather a bug in the user’s code. Although I somewhat agree with that statement, it does not offer a solution for the issues we are seeing.

I’ve created a very simple showcase of the problem that we are facing in our application:

entityManager.getTransaction().begin();
final OtherEntity other = new OtherEntity();
entityManager.persist(other);

final BaseEntity base = new BaseEntity();
base.setRef(other);
entityManager.persist(base);

entityManager.getTransaction().commit();

entityManager.setFlushMode(FlushModeType.COMMIT);
entityManager.getTransaction().begin();
entityManager.remove(other);
			
CriteriaDelete<BaseEntity> deleteStmt = entityManager
    .getCriteriaBuilder().createCriteriaDelete(BaseEntity.class);
deleteStmt.from(BaseEntity.class);
entityManager.createQuery(deleteStmt).executeUpdate();

entityManager.getTransaction().commit();

Here you can see 2 entities being persisted in the database, one referencing the other. At some later point, we want to delete other, therefore we also need to delete all entities referencing that other. Here it’s just 2 entities, but in our application it could be thousands of instances of BaseEntity referencing a single OtherEntity. Some (one or two) of these BaseEntitiy instances may have been loaded into the current Hibernate session, but certainly not all. This example triggers the TransientObjectException when the session is flushed on the final commit.

I see no way out of this situation. Removing the BaseEntity instances via EntityManager.remove would require loading them all (potentially thousands) into the session and deleting them one by one. The same is true for using EntityManager.detach: you can only detach entities that are attached and the only way to known which entities to detach is by loading them all. Using EntityManager.clear will prevent the error, but also wreak havoc on the code that is calling this code, with all of it’s (unrelated) entities suddenly being detached.

I would like to know why Hibernate performs this check at all. We’ve put foreign key constraints on all our relations in the database. There’s simply no way to delete an OtherEntity without also deleting all BaseEntity rows that reference this OtherEntity. Failure to do so would always trigger a foreign key constraint violation. Is there any way to fix this delete code, without having to load thousands of entities into the session, or perhaps we can somehow disable this check?

Best regards,
Emond Papegaaij

Hey @papegaaij, there are a few things wrong with your comments:

That is not correct, unless your application logic implies it (and so it would be needed anyway), you don’t need to delete entities referring to the removed one, but simply to remove references to the latter: setting to-one associations to null and / or removing removed entities from associated collections.

The transient check is only executed against persistent entities: i.e., instances you have already loaded in your persistence context. You will never need to load additional entity instances to fulfill this requirement, as Hibernate cannot do this check on non-existent entity instances.

This check has nothing to do with foreign keys: it’s meant to preserve a consistent state of the persistence context, or the entities you have loaded in memory in your application, which reflects the state of your database. This has always been a requirement in JPA and Hibernate ORM, and failing to respect it will cause problems with data consistency.

The only alternative to having to manually manage bi-directional associations is to have bytecode enhancement enabled and use bidirectional association management.

Yes of course, you can also nullify references. However, in this case, the reference is not null, hence the other side of the relation must also be removed. This particular case is about access tokens for an account. Every access token has an account associated, it must have. Removing the account, requires the removal of all access tokens (which could be many). We’ve also got similar cases where we can nullify the reference. However, this faces the exact same issue: setting the column to null via a CriteriaUpdate does not change the value of the property on managed entities (for obvious reasons), triggering the same TransientObjectException when performing the flush.

The cause of the issue is that a CriteriaDelete does not remove entities from the EntityManager in the same way that a CriteriaUpdate does not update entities managed by the EntityManager. The EntityManager therefore is very likely to maintain stale state after such a statement.

You can only call EntityManager.detach with a managed entity. Therefore, you must load entities in other to detach them. Going back to the example with the access tokens: if you do not know which access tokens for a given account are currently managed by the EntityManager and want to make sure the EntityManager does not manage any of these tokens, you need to do a query, load them and detach them. If you know any other way of detaching entities without any knowledge of the current content of the EntityManager, please let me know.

This question has nothing to do with bi-directional associations. The example is uni-directional. We do use bytecode enhancement, but only for dirty tracking. We’ve got other code in place to manage bi-directional associations. However, this is about uni-directional associations: the account does not have a list of its access tokens. That would not make much sense and it would be way too expensive to load into memory anyway.

The change in behavior in Hibernate 6.6.0 breaks our application in quite a few places and I really see no way of fixing the issue. It seems I can no longer rely on CriteriaUpdate and CriteriaDelete to maintain referential integrity when deleting entities. Both require me to detach any entity the EntityManager might be holding that is affected by these statements, however the EntityManager does not provide an API to do this, other than clearing the entire EntityManager or detaching all affected entities one by one (which requires loading them first).

I noticed it’s hard to describe clearly the details of the problems I’m facing. Therefore, I decided to create a simple demo project that shows the problem and some of the workarounds I’ve come up with so far. You can find the project here:

The strategies are here:

These are setup as testcases using the template provided by the Hibernate team, making them very easy to run. preHibernate66 is what we use now, and fails on Hibernate 6.6. I would really appreciate it if you could have a look at this showcase. Maybe you can come up with a strategy that removes the account and associated tokens that does not require an additional query, clearing of the session or deleting the tokens one by one.

Note that changing the Hibernate version to an older version (such as 6.5.0.Final) makes all tests pass.

The base line is this: if a TransientObjectException is thrown, it means the entities which refer to removed (i.e. transient) instances are already found in the persistence context, so there is no need to load any more than what’s already in-memory in your applications. Leaving this references hanging is not correct, and will cause problems which might even lead to data corruption, so you’re going to need to address this issues.

You can try identifying the entities to remove by queries them first, or issuing a delete query which uses the same criteria as the one that removes the associated entities: if this association is non-null, as you said, this should be mandatory in your application so this error has helped you identify a problem which should be solved.

This implies that our data layer has complete information about the context it is called from. In many cases, our data layer simply does not know what happened before it was invoke in the same transaction. The service layer might have loaded entities that the data layer will now delete. I simulated this by randomly loading 5 access tokens in my example.

Can you explain what problems you are referring to here? How can a stale entity in the persistence context cause data corruption? The entity has already been deleted from the database (using the bulk delete). Hibernate cannot flush the entity, because an update statement will fail. The second level cache for that entity should have been cleared by the bulk delete. The entity is gone, and there’s no way it will resurface once the persistence context is closed.

Did you look at my example? This is what I’ve implemented in the fetchLoadDetachStrategy. It requires an additional select query to the database to select thousands of identifiers, get references to all these entities from the persistence context one by one, and detach them. This is highly inefficient and a major degradation in performance (and simplicity) over the simple preHibernate66Strategy.

I totally understand that Hibernate cannot synchronize entities to the database that have references to unsaved transient entities. This has always been the case (or at least should have been). However, this change now also applies this check to entities that do not need to be synchronized, because they are not dirty. This has never been the case, or at least not for the past 15 years or so I’ve worked with Hibernate. It’s a major change in behavior that breaks our applications in various ways.

You say that this check is mandated by the specification, but I cannot find such a statement. All I can find is in paragraph 3.2.4:

For any entity Y referenced by a relationship from X, where the relationship to Y has not been annotated with the cascade element value cascade=PERSIST or cascade=ALL:

  • If Y is new or removed, an IllegalStateException will be thrown by the flush operation (and the transaction marked for rollback) or the transaction commit will fail.

This section is about synchronization of X and not very clear about entities that do not need to be synchronized. As said before, it’s totally clear that you cannot synchronize an entity that hold transient entities, but these entities do not need to be synchronized: they are not dirty.

1 Like