@Transactional gotchas

Here are some surprising things I’ve learned about Spring Data JPA lately.

1) When a @Transactional method calls another @Transactional method, an uncaught RuntimeException in the second method rolls back the entire transaction

Method A is annotated with @Transactional. It calls Method B (in a different class) several times. Method B is also annotated with @Transactional. Method B can fail, throwing a RuntimeException, but when calling it from Method A you don’t want that to roll back the entire transaction. Method A therefore catches any RuntimeExceptions and tidies things up before completing successfully. However, you notice that when Method A runs, any RuntimeExceptions in Method B always roll back the entire transaction, including the changes made by Method A. Why?

It’s because the default propagation on @Transactional is REQUIRED. The documentation for this describes it as follows:

Support a current transaction, create a new one if none exists

The @Transactional annotation places advice around a method (either with AspectJ or a proxy, more of that later). Some of the advice runs before the method and some after.So it works like this:

  • The “before” advice for Method A creates a transaction then calls Method A
  • Method A then calls Method B
  • The “before” advice for Method B checks to see if there’s a transaction and there is
  • Method B then runs and the exception is thrown
  • The “after” advice for Method B sees the exception bubble up through it and sets “rollback only” on the transaction. This cannot be undone
  • Thus, the entire transaction (originally created by Method A) is rolled back

Or, in a pretty picture…

transaction

Long story short, unless you handle RuntimeExceptions locally inside a @Transactional method, it’s going to roll back your entire transaction.

And why would a @Transactional method call another @Transactional method? In my case this was about code reuse. We sometimes call Method B on its own and sometimes call it as part of Method A, which carries out a larger operation.

One way to circumvent this problem might be to changed the propagation to NESTED, but…

2) The trouble with NESTED propagation

…is that JPA doesn’t support it. If you rely on JPA, and for CRUD applications it is wonderfully simple, then I’m afraid you’ll be going to bed without any nested transactions.

For transaction managers such as JDBC that do support this, it is implemented using transaction save points. Save points allow the transaction to be rolled back only as far as the last save point, which would give us exactly what we need.

If you use, for example, QueryDSL-sql with a JDBC connection manager instead of JPA, you can use nested transactions. N.B. QueryDSL-sql is not the same as QueryDSL-jpa. QueryDSL-sql is awesome and will do things like generate DTOs from the database if you let it. Very cool.

To summarise very briefly, if you set your transaction propagation to nested like this

@Transaction(propagation = Propagation.NESTED)
public void doStuff() {
    ...
}

then the transaction advice will set a save point in the current transaction when you start the method and roll back to that save point should a RuntimeException pass through it.

In the event, however, that you really want to use JPA, you could try using Propagation.REQUIRES_NEW. But, there are some…

3) Surprising things about REQUIRES_NEW

The definition of REQUIRES_NEW is as follows:

Create a new transaction, and suspend the current transaction if one exists.

It may be that I just didn’t take this quite as literally as it is intended.

If you use this propagation type in your inner method and it throws a RuntimeException, it will only roll back to the method entry point just like with NESTED. However, this is because methods annotated with REQUIRES_NEW will always create a new transaction on entry, even if one is already running. If you don’t like that surprise, I have others:

Surprise a) What happens to the transaction that’s already running? It is suspended. This means that if you’ve locked a row in your outer transaction before calling a long running REQUIRES_NEW method, that row will stay locked until the REQUIRES_NEW method returns and the outer transaction is committed or rolled back.

Surprise b) This doesn’t mean that your new transaction can see what you’ve already done in your suspended transaction. I dare say this will negate most of the advantages of having the outer transaction and the inner one run concurrently. Better to commit the first transaction before starting the second one if you can.

Surprise c) There is also a significant drawback to doing things this way. In the event that you’re running the REQUIRES_NEW method more than once, it will commit its results each time you run it. Thus, if you roll back the “outer” transaction, the successful “inner” ones (they’re not really inner, they’re separate, that’s the problem) will still have committed their changes and will not roll back with the “outer” one.

Considering these surprises, I doubt that REQUIRES_NEW is the solution to the problem most of the time. However, proxies might be…

4) Cheating with proxies can solve the problem

By default Spring creates a proxy class in order to add transaction advice to your code. Wherever your code refers to your class, Spring will instead make sure its proxy is called, which will do the transaction bits and then call your class. However, for calls that happen inside your class this won’t work because calls to the method won’t go through the proxy. That is, if you call a @Transactional method from outside its class, the transactional advice will be involved, but if you call it from inside the class it won’t.

We can use this to solve our problem, albeit in an opaque way (to anyone else who doesn’t already know about this @Transactional behaviour). If we put Method A and Method B in the same class, calls to Method B from outside the class will go through the proxy but calls from Method A won’t, so we won’t have the intermediate roll back problem from 1).

My main issue with this solution is that it is so difficult for a third party to understand. To my mind, a public method on a class should do the same thing whether it’s called from inside or outside the class. Another concern would be if someone eventually switches the Spring advice to AspectJ mode, which would break this code by making sure that the advice gets in the way even of an internal call.

5) Manual proxying

My eventual solution was to provide two Method Bs, where one is annotated @Transactional and the other isn’t (for calling by other @Transactional methods). The implementation of the @Transactional one is simply to call the other one. At least this way can always see what the behaviour should be.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s