I've been looking at differences in Hibernate exception handling for
applications that uses "native" (non-JPA) Hibernate when moving from 5.1 to
5.3.
As you know, 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.
I haven't had much of a chance to think about how we can deal with this,
but one thing that comes to mind is to allow an application to choose a
particular ExceptionConverter implementation. Hibernate would need to be
changed to always convert exceptions (including for strictly native
operations) before returning to the application.
For example, a property, hibernate.exception_converter could be added with
the following values:
jpa - default for JPA applications (org.hibernate.internal.
ExceptionConverterImpl)
native (or legacy?) - default for native applications that does not wrap
HibernateException
fully-qualified class name that implements ExceptionConverter
If users have to make changes to exception handling for 5.3, do you think
they would be willing to change their application to use JPA exceptions
(i.e., hibernate.exception_converter=jpa)?
Comments?
Regards,
Gail
[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