Tricks & Traps about Spring @Transational

When accessing and manipulating resources including databases and message queues, it's important to use transactions smartly to ensure data integrity as

A transaction will allow a set of operations to be all done ("commit" at the end) on the resource or all fail ("rollback" when error).

Therefore, I was trying to add some transaction management in my first Spring project which access a database a few months ago. I thought that would be easy - I probably just need to add something like transaction.start(), transaction.commit() and transaction.rollback(). Then I found that Spring just needs you to put @Transactional annotation before the functions and it will do the magic for you. Sounds great, right?

Well, it turned out it wasn't so simple for my project. Let's see what I have done during my debug journey.

Quick Start of Spring Transaction Management

I started by searching for a tutorial and added some annotations to my code so it looks something like this:

@Configuration
@ComponentScan(basePackages = "maisyt.demo")
public class DemoConfig {
    // ...
}

@SpringBootApplication
@EnableTransactionManagement
@Import(DemoConfig.class)
public class DemoMain {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(DemoMain.class);
        app.setWebApplicationType(WebApplicationType.NONE);
        app.run(args);
    }
}

@EnableScheduling
@Service
public class DemoScheduleTask {
    @Autowired
    DemoTask task;
    @Scheduled(fixedRateString = "${schedule.task.interval:600000}")
    public void run() {
        task.start();
    }
}

@Component
public class DemoTask {
    @Autowired
    ItemDao itemDao;

    public void start(){
        Array<Item> items = demoDao.getItems(); 
        for (Item item : items){
            try {
                handleStuff(item);
            } catch (Exception e){
                // ...
            }
        }
    }

    @Transactional
    void handleStuff(Item item) throw Exception {
        // ...
        itemDao.insert(...);
        itemDao.update(...);
        // ...
    }
}

public class ItemDao {
    public Array<Item> getItems(){
        // ...
    }

    public void update(...) throw Exception {
        // ...
    }

    public void insert(...) throw Exception {
        // ...
    }
}

However, it didn't rollback my insertion by itemDao.insert() when the itemDao.update() within the same handleStuff() call failed. By then I still didn't notice some important points mentioned in the tutorial which would have completely fixed my problem:

"Any self-invocation calls will not start any transaction, even if the method has the @Transactional annotation. Another caveat of using proxies is that only public methods should be annotated with @Transactional." [1]

Now I understand what these mean. But at that time, I proceed on trying to fix the bug by following some posts on Stack Overflow...

  1. Make all @Transactional methods public

  2. Add @Transactional to the DAO methods

But it still didn't match my expectation. The good news is, I am able to trigger a rollback on some dummy methods I created for testing and I observe something interesting in the log.

Whenever the rollback succeeded, this log entry is also shown:

2022-Nov-16 11:48:20.240 DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [oracle.jdbc.driver.T4CConnection@36480b2d] to manual commit
2022-Nov-16 11:48:20.383 DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction rollback

By default, Spring JDBC is in auto-commit mode, which creates and commits a transaction for every SQL execution. With this log entry, I know I successfully make the TransactionManager, which will disable the auto-commit mode and commit based on my annotations, start working. In other words, if it is missing, I am not starting a transaction at all. This is how I finally found out it is far from enough to use the default @Transactional annotation, the propagation type matter too.

Propagation Type

There are 7 propagation types for a transaction in spring: MANDATORY, NESTED, NEVER, NOT_SUPPORTED, REQUIRED (default), REQUIRES_NEW, SUPPORTS. [2] As stated in the document, they indicated what to do (not use a transaction / create a new transaction / merge it in the current transaction / throw exception...) if there is/isn't a transaction created when this method is called.

Apart from it, we should also pay attention to if the outer and inner transaction would rollback in the nested transaction situation (both caller and callee have @Transactional annotation). From a great article [3], I found a table and some examples that well explained different cases. If you know Chinese, I strongly recommend you to read it.

I then added the propagation type to my program...

@Component
public class DemoTask {
    public void start(){
        Array<Item> items = demoDao.getItems(); 
        for (Item item : items){
            try {
                handleStuff(item);
            } catch (Exception e){
                // ...
            }
        }
    }
    @Transactional(propagation = Propagation.REQUIRED)
    public void handleStuff(Item item) throw Exception {
        // ...
        itemDao.insert(...);
        itemDao.update(...);
        // ...
    }
}

public class ItemDao {
    @Transactional(propagation = Propagation.PROPAGATION_MANDATORY)
    public void update(...) throw Exception {
        // ...
    }
    @Transactional(propagation = Propagation.PROPAGATION_MANDATORY)
    public void insert(...) throw Exception {
        // ...
    }
}

I was so confident that it will work until I click the run test button...

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'PROPAGATION_MANDATORY'

So why is there no transaction started? I already have the Propagation.REQUIRED for the callee handleStuff()!

Self-Invocation Calls

It is because I was calling Demo.handleStuff() from Demo.start(), and they were in the same class. From Spring documentation,

In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation, in effect, a method within the target object calling another method of the target object, will not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional. Also, the proxy must be fully initialized to provide the expected behaviour so you should not rely on this feature in your initialization code, i.e. @PostConstruct. [4]

After I put handleStuff() in a new class and add the noRollbackFor = Exception.class at the correct places, I finally got the expected result.

Conclusion

Be careful of the following when using Spring transaction management:

  1. Make the @Transactional method public

  2. Set the propagation type properly, especially when transactions are nested

  3. Make sure there are no self-invocation calls unless using aspectj mode

Reference

  1. https://www.baeldung.com/transaction-configuration-with-jpa-and-spring

  2. https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Propagation.html

  3. https://www.tpisoftware.com/tpu/articleDetails/2741 (In Chinese)

  4. https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html