Add an option for restoring 5.1 native exception handling

Description

Exception handling changed in 5.2 when hibernate-entitymanager was merged into hibernate-core. This change is documented in the 5.2 migration guide. [1]

Here is the relevant text:

"org.hibernate.HibernateException now extends javax.persistence.PersistenceExceptions. Hibernate methods that "override" methods from their JPA counterparts now will also throw various JDK defined RuntimeExceptions (such as IllegalArgumentException, IllegalStateException, etc) as required by the JPA contract."

While digging into this, I see that a HibernateException thrown when using 5.1 may, in 5.3, be unwrapped, or may be wrapped by a PersistenceException (or a subclass), IllegalArgumentException, or IllegalStateException, depending on the particular operation being performed when the HibernateException is thrown.

The reason why the exceptions may be wrapped or unwrapped is because Hibernate converts exceptions (via AbstractSharedSessionContract#exceptionConverter) for JPA operations which may wrap the HibernateException or throw a different exception. Hibernate does not convert exceptions from strictly "native" operations, so those exceptions remain unwrapped.

Here are a couple of examples to illustrate:

1) A HibernateException (org.hibernate.TransientObjectException) was thrown in 5.1. In 5.3, the same condition can result in org.hibernate.TransientObjectException that is either unwrapped, or wrapped by IllegalStateException. I've pushed a test to my fork to illustrate. [2]

Thrown during Session#save, #saveOrUpdate
In 5.1: org.hibernate.TransientObjectException (unwrapped)
In 5.3: org.hibernate.TransientObjectException (unwrapped)

Thrown during Session#persist, #merge, #flush
In 5.1, org.hibernate.TransientObjectException (unwrapped)
In 5.3, org.hibernate.TransientObjectException wrapped by IllegalStateException

2) A HibernateException thrown when using 5.1 may be wrapped by a PersistenceException, even though HibernateException already extends PersistenceException.

For example, see TransactionTimeoutTest#testTransactionTimeoutFailure:
https://github.com/hibernate/hibernate-orm/blob/master/hibernate-core/src/test/java/org/hibernate/test/tm/TransactionTimeoutTest.java#L60-L82

The exception thrown by the test indicates that the transaction timed out. This exception is important enough that an application might retry the operation, or at least log for future investigation.

Thrown during Session#persist (or when changed to use Session#merge or Session#flush):
In 5.1: org.hibernate.TransactionException (unwrapped)
In 5.3: org.hibernate.TransactionException wrapped by javax.persistence.PersistenceException

Thrown if the test is changed to use Session#save or #saveOrUpdate instead:
In 5.1: org.hibernate.TransactionException (unwrapped)
In 5.3: org.hibernate.TransactionException (unwrapped)

Similarly, by adding some logging, I see that HibernateException objects can be wrapped by PersistenceException when running the 5.3 hibernate-core unit tests. Depending on the context, I see that some of the following exceptions can be wrapped or unwrapped.

org.hibernate.exception.ConstraintViolationException
org.hibernate.exception.DataException
org.hibernate.exception.GenericJDBCException
org.hibernate.exception.SQLGrammarException
org.hibernate.id.IdentifierGenerationException
org.hibernate.loader.custom.NonUniqueDiscoveredSqlAliasException
org.hibernate.PersistentObjectException
org.hibernate.PropertyAccessException
org.hibernate.PropertyValueException
org.hibernate.TransactionException

You can see an example using org.hibernate.exception.ConstraintViolationException at [3].

In order to deal with these differences, an application could change the following (which was appropriate for 5.1):

try {
...
}
catch (HibernateException ex) {
procressHibernateException( ex );
}

to the following for 5.3:

try {
...
}
catch (PersistenceException | IllegalStateException | IllegalArgumentException ex) {
if ( HibernateException.class.isInstance( ex ) ) {
handleHibernateException( (HibernateException) ex );
}
else if ( HibernateException.class.isInstance( ex.getCause() ) ) {
handleHibernateException( (HibernateException) ex.getCause() );
}
}

IMO, it's a little clumsy having to deal with both wrapped and unwrapped exceptions. It would be better if exceptions were consistently wrapped, or consistently unwrapped.

To deal with this, hibernate.native_exception_handling_51_compliance will be added with possible values true or false (default). This property will indicate if exception handling for a SessionFactory built via Hibernate's native bootstrapping should behave the same as native exception handling in Hibernate ORM 5.1,

When set to true, HibernateException will not be wrapped or converted according to the JPA specification.

This setting will be ignored for a SessionFactory built via JPA bootstrapping.

[1] https://github.com/hibernate/hibernate-orm/blob/5.2/migration-guide.adoc
[2] https://github.com/gbadner/hibernate-core/blob/exception-compatibility/orm/hibernate-orm-5/src/test/java/org/hibernate/bugs/ORMTransientObjectExceptionUnitTestCase.java
[3] https://github.com/gbadner/hibernate-core/blob/exception-compatibility/orm/hibernate-orm-5/src/test/java/org/hibernate/bugs/ORMConstraintViolationExceptionUnitTestCase.java

Environment

None

Status

Assignee

Gail Badner

Reporter

Gail Badner

Fix versions

Labels

backPortable

None

Suitable for new contributors

None

Requires Release Note

None

Pull Request

None

backportDecision

None

Components

Priority

Major
Configure