why does transaction roll back on RuntimeException but not SQLException
Asked Answered
S

4

37

I have a Spring-managed service method to manage database inserts. It contains multiple insert statements.

@Transactional
public void insertObservation(ObservationWithData ob) throws SQLException 
{
    observationDao.insertObservation(ob.getObservation());
            // aop pointcut inserted here in unit test
    dataDao.insertData(ob.getData());
}

I have two unit tests which throw an exception before calling the second insert. If the exception is a RuntimeException, the transaction is rolled back. If the exception is a SQLException, the first insert is persisted.

I'm baffled. Can anyone tell me why the transaction does not roll back on a SQLException? Can anyone offer a suggestion how to manage this? I could catch the SQLException and throw a RuntimeException, but that just seems weird.

Salonika answered 19/8, 2011 at 18:30 Comment(1)
Per skaffman's answer and Nathan's tip, I've caught all the SQLExceptions at the DAO layer and used Spring's SQLExceptionTranslator to translate them into unchecked DataAccessExceptions. This Spring forum post, forum.springsource.org/…, and Robert Martin's Error Handling chapter in "Clean Code. A Handbook of Agile Software Craftsmanship" (Prentice Hall, 2008) were also helpful to clarify why throwing the unchecked exception maintains a nicer modularity.Salonika
A
59

This is defined behaviour. From the docs:

Any RuntimeException triggers rollback, and any checked Exception does not.

This is common behaviour across all Spring transaction APIs. By default, if a RuntimeException is thrown from within the transactional code, the transaction will be rolled back. If a checked exception (i.e. not a RuntimeException) is thrown, then the transaction will not be rolled back.

The rationale behind this is that RuntimeException classes are generally taken by Spring to denote unrecoverable error conditions.

This behaviour can be changed from the default, if you wish to do so, but how to do this depends on how you use the Spring API, and how you set up your transaction manager.

Anesthesiologist answered 19/8, 2011 at 18:38 Comment(1)
Yes, this is correct, although I don't entirely understand this. I want to throw the checked SQLException so the application code can handle it (log, move on, stop execution, etc.), but I don't want the transaction to succeed. I can set each transaction annotation to roll back for the exception, but I'd prefer to set up the transaction manager to do that application-wide. It's not immediately apparent that I can configure the basic DataSourceTransactionManager to do that. If you have a suggestion of a richer discussion of this, I'd appreciate it. I'm diving into the spring forums now. Thanks!Salonika
S
5

For @Transactional, by default, rollback happens for runtime, unchecked exceptions only. Thus, your checked exception SQLException does not trigger a rollback of the transaction; the behavior can be configured with the rollbackFor and noRollbackFor annotation parameters.

@Transactional(rollbackFor = SQLException.class)
public void insertObservation(ObservationWithData ob) throws SQLException 
{
    observationDao.insertObservation(ob.getObservation());
            // aop pointcut inserted here in unit test
    dataDao.insertData(ob.getData());
}
Steamroller answered 2/8, 2018 at 5:55 Comment(1)
I think this will make the transaction to only rollback for SQL Exception and its sub classes but not for other types.Ramah
P
2

Spring makes extensive use of RuntimeExceptions (including using DataAccessExceptions to wrap SQLExceptions or exceptions from ORMs) for cases where there's no recovering from the exception. It assumes you want to use checked exceptions for cases where a layer above the service needs to be notified of something, but you don't want the transaction to be interfered with.

If you're using Spring you might as well make use of its jdbc-wrapping libraries and DataAccessException-translating facility, it will reduce the amount of code you have to maintain and provide more meaningful exceptions. Also having a service layer throwing implementation-specific exceptions is a bad smell. The pre-Spring way was to create implementation-agnostic checked exceptions wrapping implementation-specific exceptions, this resulted in a lot of busy-work and a bloated code base. Spring's way avoids those problems.

If you want to know why Spring chose to make things work this way, it's probably because they use AOP to add the transaction-handling. They can't change the signature of the method they are wrapping, so checked exceptions aren't an option.

Peachey answered 19/8, 2011 at 18:44 Comment(0)
C
0

If an unchecked exception (any exception extends RunTimeException) is thrown from the method that has spring transactional annotation, the transaction will be rolled back. If a checked exception (IOException, SQLException etc.) is thrown, the transaction will not be rolled back. You can use https://projectlombok.org/features/SneakyThrows from lombok, If you want to the transaction rolled back any kind of exception.

Culley answered 3/2, 2022 at 13:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.