codete-5-Common-Spring-@Transactional-Pitfalls-[Spring-_-JPA-Pitfalls-Series]_1-main

The article is a part of the JPA & Spring pitfalls series, which you can check out here.

 

Preface

The @Transactional annotation is probably the most randomly used annotation in the whole Java development world - and it’s terrifying! 

I noticed that not knowing why it is usually perceived as a kind of magical annotation. How many times I have seen those StackOverflow answers suggesting “Have you tried adding @Transactional?” with any further explanation or have heard developers debugging a piece of unworking code and saying to each other: “Eh, maybe something changes when we add this, you know, @Transactional thing here?”. 

But the worst part is that sometimes it seems like it fixed the problem, so developers just leave it and don’t try to find out what it really changed and whether it didn’t break anything somewhere else. In this article, I’ll try to outline some of the most common misunderstandings related to @Transactional. I’m assuming that you are familiar with Spring, JPA and Spring Data. Hopefully, after this read, this annotation won’t be magical to you any longer.

 

Which @Transactional?

Although @Transactional is present in Spring and JavaEE (javax.transaction package), we’ll be using the one from Spring Framework. It’s generally a better practice since it is more natural to Spring applications and at the same time offers more options like timeout, isolation, etc.

 

@Transactional - a quick recap

To recap, always when you see a method like this:

@Transactional
public void registerNewAccount() {
   // business code
}

you should remember that when you call such a method, the invocation in fact will be wrapped with transaction handling code similar to this:

UserTransaction userTransaction = entityManager.getTransaction();
try {
  // begin a new transaction if expected
  // (depending on the current transaction context and/or propagation mode setting)
   userTransaction.begin(); 

   registerNewAccount(); // the actual method invocation

   userTransaction.commit();
} catch(RuntimeException e) {
   userTransaction.rollback(); // initiate rollback if business code fails
   throw e;
}

Of course, this code is a simplification of what really happens in the background, but it should be good enough to visualize and memorize.

 

Redundant @Transactional or JPA calls

This isn’t actually any trap or a serious mistake, but I see it very often during code review. Please look at this code first:

@Transactional
public void changeName(long id, String name) {
  User user = userRepository.getById(id);
  user.setName(name);
  userRepository.save(user);
}

At first sight, there’s nothing wrong with this code and indeed it works perfectly fine in terms of functionality. However, it instantly reveals that the author wasn’t sure about how @Transactional works. 

When a method is transactional, then entities retrieved within this transaction are in the managed state, which means that all changes made to them will be populated to the database automatically at the end of the transaction. Therefore either the save() call is redundant and the code should look like this:

@Transactional
public void changeName(long id, String name) {
  User user = userRepository.getById(id);
  user.setName(name);
}

or, if we don’t need to perform this within a transaction, it could be:

public void changeName(long id, String name) {
  User user = userRepository.getById(id);
  user.setName(name);
  userRepository.save(user);
}

What is more important, besides only the general misconception, writing a code like this may lead to other problems in the future. Imagine that at some point someone would like to add a validation that we should change the user name only when it’s not empty and writes a code like this:

@Transactional
public void changeName(long id, String name) {
  User user = userRepository.getById(id);
  user.setName(name);
  if (StringUtils.isNotEmpty(name)) {
    userRepository.save(user);
  }
}

It looks okay, but it won’t work as someone expected. 

Since this is a transactional method, the user entity is in managed state and therefore name change will be populated to the database anyways. If you imagine that the @Transactional annotation isn’t on the method-level, but on the class level, then it would be even more difficult to catch that there’s something wrong with this code.

 

@Transactional ignored?

Have you ever annotated a method with @Transactional (or e.g. @Async) and it didn’t work? As if it was totally ignored by Spring? This is because annotations like these can’t be (at least by default) put on any method, two conditions must be met to let Spring take action:

  1. The method visibility can’t be any other than public.
  2. The invocation must come from outside of the bean.

This is due to how Spring proxying work by default (using CGLIB proxies). 

I won’t dive into details, because it’s a topic wide enough to write another article, but generally speaking, when you auto-wire a bean of type Sample, Spring in fact doesn’t provide you exactly with an instance of Sample. Instead, it injects a generated proxy class that extends Sample (yes, that’s the reason why you can’t make your spring bean classes final) and overrides its public methods to be able to add extra behaviour (like transactional support).

That’s why methods with @Transactional must be public (so Spring can easily override them) and also that’s why the invocation must come from outside (only then it may go through a proxy, Spring can’t replace “this” reference with a proxy reference).

Solution 1

Extract the method to another class and make it public.

Solution 2

Use AspectJ weaving instead of default proxy-based Spring AOP. AspectJ is capable of working with both: non-public methods and self-invocations.

Solution 3 (only for self-invocation)

Disclaimer: I wouldn’t use this “solution” in the production code, because it’s more error-prone, harder to understand quickly and forces using field injection. Anyways, I find it interesting enough to give you at least an overview and describe it.


The following code fails when we call userService.createUser(“test”) because of ignored @Transactional (due to self-invocation): 

@Service
public class UserService {


   @PersistenceContext
   private EntityManager entityManager;
  
   public User createUser(String name) {
       User newUser = new User(name);
       return this.saveUser(newUser);
   }


   @Transactional
   public User saveUser(User newUser) {
       entityManager.persist(newUser);
       return newUser;
   }


}

Since you can’t use self-invocation, because Spring is not able to intercept “this”, you can autowire a “self” proxy reference and use it instead of “this”:

@Service
public class UserService {


   @PersistenceContext
   private EntityManager entityManager;


   @Autowired
   private UserService _self;


   public User createUser(String name) {
       User newUser = new User(name);
       return _self.saveUser(newUser);
   }


   @Transactional
   public User saveUser(User newUser) {
       entityManager.persist(newUser);
       return newUser;
   }


}

 

@Transactional(readOnly = true)

Firstly, the readOnly parameter doesn’t guarantee its behaviour, is only a hint that may or may not be taken into account. From documentation:

“This just serves as a hint for the actual transaction subsystem; it will not necessarily cause the failure of write access attempts. A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction but rather silently ignore the hint.”

Therefore, its behaviour may vary between JPA implementations and even between their versions - and it really happens, at least in Hibernate, it has changed a few times in the last few years. So more than relying on it too much, better make sure that your code does just pure reads within a specific transaction if you expect that. In Hibernate, currently, it causes setting Session’s FlushType to MANUAL, which means that the transaction won’t be committed and thus any modifying operations will be silently ignored.

 

Depends on the propagation setting

Another important thing to remember is that the readOnly hint will be applied only when the corresponding @Transactional causes starting a completely new transaction.  Therefore, it’s closely related to the propagation setting. For example: for SUPPORT, readOnly flag won’t ever be used; for REQUIRES_NEW always; for REQUIRED it depends on whether we already are in the transactional context or not, etc.

If you don’t feel comfortable with transaction propagation settings, you might be interested in another article of mine.

 

Is it worth using this param for truly read-only transactions?

Generally, we should simply avoid starting DB transactions for read-only operations as they are unnecessary, can lead to database deadlocks, worsen performance and throughput. The only argument for starting read-only transactions that I can possibly see is the recent cache memory consumption optimization since Spring 5.1 proposed by Vlad Mihalcea.

 

Rollbacks

The rule when transaction rollbacks are triggered automatically is very simple but worth reminding: by default, a transaction will be rolled back if any unchecked exception is thrown within it, whereas checked exceptions don’t trigger rollbacks.

We can customize this behaviour with parameters:

  • noRollbackFor - to specify runtime exception, which shouldn’t cause a rollback
  • rollbackFor - to indicate which checked exception should trigger rollbacks

 

Propagation modes and isolation levels

Propagation and isolation are two advanced and essential topics related to transactions and their relationship to each other. 

If you don’t feel at least comfortable with basic use cases of each, I firmly recommend you making up for it, because experimenting with them randomly (what I sometimes see on StackOverflow) may cost you much more time spent on debugging, since this kind of bugs is exceptionally hard to figure out.

If you already know about propagation, make sure to check out another article with a few practical examples of what may happen if you mix propagation modes not carefully enough.

 

Spring @Transactional - wrap up

  • Any change made within a transaction to an entity (retrieved within the same transaction) will automatically be populated to the database at the end of the transaction, without the need for explicit manual updates.
  • Don’t write redundant JPA calls or @Transactional just “for more safety”. It may bring more risk than safety.
  • @Transactional works only when the annotated method is public and invoked from another bean. Otherwise, the annotation will be silently ignored.
  • As a rule of thumb: don’t use readOnly = true parameter until it’s really necessary.
  • By default, only unchecked exceptions trigger rollbacks, checked exceptions do not. It can be customized with rollbackFor and noRollbackFor parameters.
  • Learn how different isolation levels and propagation modes work. It may save you a lot of time one day.

Thanks for reaching that far, I hope that you’ve enjoyed reading!

Tech Lead at Codete. Michał is an eager fan of the fresh approach to Java programming. Enthusiast of Spring tech stack and refactoring techniques. Enjoys solving Java quirks, algorithmic puzzles, and... Rubik's cube. Privately, amateur drummer.