Managing Transactions with Spring and Spring Data JPA


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Spring Boot and Spring Data JPA make the handling of transactions extremely simple. They enable you to declare your preferred transaction handling and provide seamless integration with Hibernate and JPA.

The only thing you need to do is to annotate one of your methods with @Transactional. But what does that actually do? Which method(s) should you annotate with @Transactional? And why can you set different propagation levels?

I will answer all of these questions in this article. To make that a little bit easier to understand, I will focus on local transactions. These are transactions between your application and 1 external system, e.g., your database. From a Spring application point of view, the same concepts also apply to distributed transactions. So, if you’re using distributed transactions, I recommend that you keep reading and research the required configuration parameters for distributed transactions afterward.

OK, before we can talk about Spring’s transaction support, we need to take a step back and explain database transactions in general and take a quick look at JDBC’s transaction management. This is necessary because Spring’s transaction management is based on the transaction management provided by your database and the JDBC specification.

What is a transaction?

Transactions manage the changes that you perform in one or more systems. These can be databases, message brokers, or any other kind of software system. The main goal of a transaction is to provide ACID characteristics to ensure the consistency and validity of your data.

ACID transactions

ACID is an acronym that stands for atomicity, consistency, isolation, and durability:

  • Atomicity describes an all or nothing principle. Either all operations performed within the transaction get executed or none of them. That means if you commit the transaction successfully, you can be sure that all operations got performed. It also enables you to abort a transaction and roll back all operations if an error occurs.
  • The consistency characteristic ensures that your transaction takes a system from one consistent state to another consistent state. That means that either all operations were rolled back and the data was set back to the state you started with or the changed data passed all consistency checks. In a relational database, that means that the modified data needs to pass all constraint checks, like foreign key or unique constraints, defined in your database.
  • Isolation means that changes that you perform within a transaction are not visible to any other transactions until you commit them successfully.
  • Durability ensures that your committed changes get persisted.

As you can see, a transaction that ensures these characteristics makes it very easy to keep your data valid and consistent.

Relational databases support ACID transactions, and the JDBC specification enables you to control them. Spring provides annotations and different transaction managers to integrate transaction management into their platform and to make it easier to use. But in the end, it all boils down to the features provided by these lower-level APIs.

Using transactions with JDBC

There are 3 main operations you can do via the java.sql.Connection interface to control an ACID transaction on your database.

try (Connection con = dataSource.getConnection()) {
    con.setAutoCommit(false);

    // do something ...
	
    con.commit();
} catch (SQLException e) {
    con.rollback();
}

You can:

  • Start a transaction by getting a Connection and deactivating auto-commit. This gives you control over the database transaction. Otherwise, you would automatically execute each SQL statement within a separate transaction.
  • Commit a transaction by calling the commit() method on the Connection interface. This tells your database to perform all required consistency checks and persist the changes permanently.
  • Rollback all operations performed during the transaction by calling the rollback() method on the Connection interface. You usually perform this operation if an SQL statement failed or if you detected an error in your business logic.

As you can see, conceptually, controlling a database transaction isn’t too complex. But implementing these operations consistently in a huge application, it’s a lot harder than it might seem. That’s where Spring’s transaction management comes into play.

Managing Transactions with Spring

Spring provides all the boilerplate code that’s required to start, commit, or rollback a transaction. It also integrates with Hibernate’s and JPA’s transaction handling. If you’re using Spring Boot, this reduces your effort to a @Transactional annotation on each interface, method, or class that shall be executed within a transactional context.

If you’re using Spring without Spring Boot, you need to activate the transaction management by annotating your application class with @EnableTransactionManagement.

Here you can see a simple example of a service with a transactional method.

@Service 
public class AuthorService {     
	private AuthorRepository authorRepository;     
	
	public AuthorService(AuthorRepository authorRepository) {         		
		this.authorRepository = authorRepository;     
	}     
	
	@Transactional     
	public void updateAuthorNameTransaction() {         
		Author author = authorRepository.findById(1L).get(); 
        author.setName("new name");     
	} 
}

The @Transactional annotation tells Spring that a transaction is required to execute this method. When you inject the AuthorService somewhere, Spring generates a proxy object that wraps the AuthorService object and provides the required code to manage the transaction.

By default, that proxy starts a transaction before your request enters the first method that’s annotated with @Transactional. After that method got executed, the proxy either commits the transaction or rolls it back if a RuntimeException or Error occurred. Everything that happens in between, including all method calls, gets executed within the context of that transaction.

The @Transactional annotation supports a set of attributes that you can use to customize the behavior. The most important ones are propagation, readOnly, rollbackFor, and noRollbackFor. Let’s take a closer look at each of them.

Defining Transaction Propagation

Spring’s Propagation enum defines 7 values that you can provide to the propagation attribute of the @Transactional annotation.

@Service
public class AuthorService {

    private AuthorRepository authorRepository;

    public AuthorService(AuthorRepository authorRepository) {
        this.authorRepository = authorRepository;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateAuthorNameTransaction() {
        Author author = authorRepository.findById(1L).get();
        author.setName("new name");
    }
}

They enable you to control the handling of existing and creation of new transactions. You can choose between:

  • REQUIRED to tell Spring to either join an active transaction or to start a new one if the method gets called without a transaction. This is the default behavior.
  • SUPPORTS to join an activate transaction if one exists. If the method gets called without an active transaction, this method will be executed without a transactional context.
  • MANDATORY to join an activate transaction if one exists or to throw an Exception if the method gets called without an active transaction.
  • NEVER to throw an Exception if the method gets called in the context of an active transaction.
  • NOT_SUPPORTED to suspend an active transaction and to execute the method without any transactional context.
  • REQUIRES_NEW to always start a new transaction for this method. If the method gets called with an active transaction, that transaction gets suspended until this method got executed.
  • NESTED to start a new transaction if the method gets called without an active transaction. If it gets called with an active transaction, Spring sets a savepoint and rolls back to that savepoint if an Exception occurs.

Using Read-Only Transactions

If you want to implement a read-only operation, I recommend using a DTO projection. It enables you to only read the data you actually need for your business code and provides a much better performance.

But if you decide to use an entity projection anyways, you should at least mark your transaction as read-only. Since Spring 5.1, this sets Hibernate’s query hint org.hibernate.readOnly and avoids dirty checks on all retrieved entities.

@Service
public class AuthorService {

    private AuthorRepository authorRepository;

    public AuthorService(AuthorRepository authorRepository) {
        this.authorRepository = authorRepository;
    }

    @Transactional(readOnly = true)
    public Author getAuthor() {
        return authorRepository.findById(1L).get();
    }
}

Handling Exceptions

I explained earlier, that the Spring proxy automatically rolls back your transaction if a RuntimeException or Error occurred. You can customize that behavior using the rollbackFor and noRollbackFor attributes of the @Transactional annotation.

As you might guess from its name, the rollbackFor attribute enables you to provide an array of Exception classes for which the transaction shall be rolled back. And the noRollbackFor attribute accepts an array of Exception classes that shall not cause a rollback of the transaction.

In the following example, I want to roll back the transaction for all subclasses of the Exception class except the EntityNotFoundException.

@Service
public class AuthorService {

    private AuthorRepository authorRepository;

    public AuthorService(AuthorRepository authorRepository) {
        this.authorRepository = authorRepository;
    }

    @Transactional
		(rollbackFor = Exception.class, 
		 noRollbackFor = EntityNotFoundException.class)
    public void updateAuthorName() {
        Author author = authorRepository.findById(1L).get();
        author.setName("new name");
    }
}

Conclusion

Spring Boot and Spring Data JPA provide an easy to use transaction handling. You only need to annotate your interface, class, or method with Spring’s @Transactional annotation. Spring then wraps your service in a generated proxy that joins an active transaction or starts a new one and commits or rolls the transaction back after your method got executed.

You can customize the default behavior using the propagation, readOnly, rollbackFor, and noRollbackFor attributes:

  • The propagation attribute gives you control over the handling of existing and the creation of new transactions. If your method gets called within the context of an activate transaction, you can, for example, decide if your method shall join that transaction, create a new one, or fail.
  • You can use the readOnly attribute to improve the performance of read-only operations.
  • The rollbackFor and noRollbackFor attributes enable you to define which Exception classes will cause a rollback of your transaction and which can be handled by your business logic.

4 Comments

  1. Excellent. Thank you for the article.

  2. Very nice and clear explanation. Great JOB.

    1. Avatar photo Thorben Janssen says:

      Thanks

  3. Hello Torsten,
    Thanks again for your nice explanation.
    I have a question to annotated transactions. What happens with Exceptions that are not defined at all?
    (rollbackFor = Exception.class,
    noRollbackFor = EntityNotFoundException.class)

    Or is best practice just to use only either rollbackFor or the other noRollbackFor?

    Kind regards,
    Stephan

Comments are closed.