6

My Spring/Java web application has @Transactional services that can touch the database:

@Transactional
public class AbstractDBService  { ... }

Desired functionality is for any uncaught throwable that propagates up beyond the service layer to cause a rollback. Was a bit surprised this isn't the default behaviour but after a bit of googling tried:

@Transactional(rollbackFor = Exception.class)

This seems to work except when an exception is deliberately swallowed and not rethrown. (The particular case is when an entity is not found. Guess this could be redesigned to not throw an Exception but expect there will inevitably be others - e.g. one that springs to mind is an InterruptedException when using Thread.sleep()). Then Spring complains:

org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly ...truncated.. Caused by: javax.persistence.RollbackException: Transaction marked as rollbackOnly at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:58) at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:517)

Am I missing something here?... Is there a way to tell Spring to rollback on all uncaught throwables?

Steve Chambers
  • 37,270
  • 24
  • 156
  • 208
  • I'm confused. Your current `rollbackFor` should cause the `@Transactional` proxy to rollback for any `Exception` that gets thrown from the annotated method. Is that not what you see? – Sotirios Delimanolis Nov 04 '15 at 17:37
  • I am seeing that but it *also* seems to make an abortive attempt to rollback when such an `Exception` is caught and not rethrown. ([This answer](http://stackoverflow.com/questions/19302196/transaction-marked-as-rollback-only-how-do-i-find-the-cause#19311268) seems to back this up.) – Steve Chambers Nov 04 '15 at 17:42

1 Answers1

13

If you want to rollback on all uncaught Throwables, you can specify that in the annotation:

@Transactional(rollbackFor = Throwable.class)

By default Spring doesn't rollback for Error subclasses, probably because it seems doubtful once an Error is thrown that the JVM will be in a good enough state to do anything about it anyway, at that point the transaction can just time out. (If you try to rollback when an OutOfMemoryError is raised, the most likely outcome is another OutOfMemoryError.) So you may not gain much with this.

When you mention the case of swallowing an exception, there's no way Spring can be expected to know about it because the exception is not finding its way to Spring's proxy (which is implementing the transactional functionality). This is what happens in your RollbackException example, Hibernate has figured out the transaction needs to rollback but Spring didn't get the memo because somebody ate the exception. So Spring isn't rolling the transaction back, it thinks everything is ok and tries to commit, but the commit fails due to Hibernate having marked the transaction rollback-only.

The answer is to not swallow those exceptions but let them be thrown; making them unchecked is supposed to make it easier for you to do the right thing. There should be an exception handler set up to receive exceptions thrown from the controllers, most exceptions thrown at any level of the application can be caught there and logged.

Nathan Hughes
  • 94,330
  • 19
  • 181
  • 276
  • Many thanks for the detailed answer. Some good insights into how Spring works under the bonnet - to me this is all "magic"... but once you start looking guess some types of magic are possible and some just aren't! Still think it's a bit of a shame though as what if e.g. a third party utility library were used where an exception were thrown that we don't care about and want to swallow and continue. OK, could start adding `noRollbackFor` for specific exceptions but what if it is a widely common exception type?... Anyway this is a bit hypothetical and sounds like it's an architectural limitation. – Steve Chambers Nov 05 '15 at 08:57
  • @Steve: you can still do whatever exception handling you need, including eating the exception. just be aware what you want the effect to be on the current transaction. btw spring by default lets you avoid rolling back for checked exceptions. (really the defaults work pretty well for a lot of cases, as I described in the first part about rolling back for Error.) – Nathan Hughes Nov 05 '15 at 14:10