Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Spring Boot Transaction Management: A Comprehensive Guide

Tech May 9 3

Introduction

This article provides a comprehensive guide to using transaction management in Spring Boot applications.

Spring Boot Transaction Management

Note: If you wish to directly obtain the project code, you can jump to the bottom of the article and download it via the link.

What is a Transaction?

A transaction is a sequence of operations performed as a single logical unit of work. It must be atomic, consistent, isolated, and durable (ACID).

Transaction Management Methods

Spring supports two types of transaction management:

  • Programmatic Transaction Management: Uses TransactionTemplate or the underlying PlatformTransactionManager. Spring recommends using TransactionTemplate.
  • Declarative Transaction Management: Built on AOP. It intercepts method calls to create or join a transaction before the target method starts and commits or rolls back after execution. This method is recommended because it does not require code changes; simply adding @Transactional enables it.

Transaction Commit Behavior

By default, databases operate in auto-commit mode, where each statement is a separate transaction. For a set of related operations to be grouped into a single transaction, auto-commit must be disabled. Spring does this automatically by setting connection.setAutoCommit(false) and then explicitly calling connection.commit().

Transaction Isolation Levels

Isolation levels define how transactions interact with each other. The TransactionDefinition interface defines five constants:

  • TransactionDefinition.ISOLATION_DEFAULT: Uses the default isolation level of the underlying database. For most databases, this is READ_COMMITTED.
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED: Allows a transaction to read uncommitted changes from other transactions. This level does not prevent dirty reads, non-repeatable reads, or phantom reads and is rarely used.
  • TransactionDefinition.ISOLATION_READ_COMMITTED: Prevents dirty reads by only allowing a transaction to read committed changes from other transactions. This is the recommended level for most cases.
  • TransactionDefinition.ISOLATION_REPEATABLE_READ: Ensures that if a transaction reads the same data multiple times, it will get the same result. This prevents dirty reads and non-repeatable reads.
  • TransactionDefinition.ISOLATION_SERIALIZABLE: Transactions are executed sequentially, preventing dirty reads, non-repeatable reads, and phantom reads. However, this severely impacts performance.

Transaction Propagation Behaviors

Propagation behavior defines how a transactional method behaves when it is called within an existing transaction context. The TransactionDefinition interface defines the following constants:

  • TransactionDefinition.PROPAGATION_REQUIRED: Joins the current transaction if one exists; otherwise, creates a new one. This is the default.
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW: Creates a new transaction, suspending the current one if it exists.
  • TransactionDefinition.PROPAGATION_SUPPORTS: Joins the current transaction if one exists; otherwise, runs non-transactionally.
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: Runs non-transactionally, suspending the current transaction if one exists.
  • TransactionDefinition.PROPAGATION_NEVER: Runs non-transactionally and throws an exception if a transaction exists.
  • TransactionDefinition.PROPAGATION_MANDATORY: Joins the current transacsion if one exists; throws an exception if there is no existing transaction.
  • TransactionDefinition.PROPAGATION_NESTED: If a current transaction exists, runs within a nested transaction; otherwise, behaves like PROPAGATION_REQUIRED.

Transaction Rollback Rules

By default, the Spring transaction manager rolls back a transaction for unchecked exceptions (subclasses of RuntimeException and Error). Checked exceptions do not trigger a rollback. You can configure specific exceptions to roll back or not roll back.

Common Transaction Attributes

  • readOnly: Marks the transaction as read-only (true) or read-write (false). Default is false. Example: @Transactional(readOnly=true)
  • rollbackFor: Array of exception classes that trigger a rollback. Example: @Transactional(rollbackFor=RuntimeException.class) or @Transactional(rollbackFor={RuntimeException.class, Exception.class})
  • rollbackForClassName: Array of exception class names that trigger a rollback.
  • noRollbackFor: Array of exception classes that do not trigger a rollback.
  • noRollbackForClassName: Array of exception class names that do not trigger a rollback.
  • propagation: Sets the propagation behavior. Example: @Transactional(propagation=Propagation.NOT_SUPPORTED, readOnly=true)
  • isolation: Sets the isolation level. The default value is usually sufficient.
  • timeout: Sets the transaction timeout in seconds. A value of -1 means never time out.

Important Considerations for Transactions

  1. Determine whether transactions are needed before writing code to avoid maintenance difficulties later.
  2. Always test transactions, as they may not work as expected.
  3. The @Transactional annotation should only be used on public methods. Using it on protected or private methods will not cause an error but will be silently ignored, and the transaction will not work.
  4. @Transactional does not apply to sub-methods called within the annotated method. If you want a sub-method to participate in the transaction, either use rollbackFor on the sub-method or have the sub-method throw an exception that is handled by the calling method.
  5. If a transaction method catches an exception, the transaction will not be rolled back unless you manually roll back by calling TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() or rethrow the exception (e.g., throw new RuntimeException()).

Setup and Configuration

Environment Requirements

  • JDK: 1.8
  • Spring Boot: 1.5.17.RELEASE

Maven Dependencies (pom.xml)

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.17.RELEASE</version>
    <relativePath/>
</parent>

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Boot Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- DevTools -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- MyBatis Spring Boot Starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.2.0</version>
    </dependency>
    <!-- MySQL Connector -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.44</version>
    </dependency>
    <!-- Druid Connection Pool -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.8</version>
    </dependency>
</dependencies>

Application Properties (application.properties)

banner.charset=UTF-8
server.tomcat.uri-encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.messages.encoding=UTF-8
spring.application.name=springboot-transactional
server.port=8182

spring.datasource.url=jdbc:mysql://localhost:3306/springBoot?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.filters=stat,wall,log4j
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

logging.level.com.pancm.dao=debug

Code Implementation

To use transaction management in Spring Boot, add @EnableTransactionManagement to the main application class and @Transactional to the service layer's public methods.

Example 1: Basic Usage

The simplest way to use @Transactional is to add it to a public method and let it throw exceptions that Spring will handle.

@Transactional
public boolean test1(User user) throws Exception {
    long id = user.getId();
    System.out.println("First query result: " + userDao.findById(id));
    // Insert the same user twice to cause a primary key conflict and test rollback
    userDao.insert(user);
    System.out.println("Second query result: " + userDao.findById(id));
    userDao.insert(user);
    return false;
}

Example 2: Manual Rollback

If you want to handle exceptions yourself, you can manually roll back the transaction by calling TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() in the catch block. Ensure this is called immediately after the exception is caught.

@Transactional
public boolean test2(User user) {
    long id = user.getId();
    try {
        System.out.println("First query result: " + userDao.findById(id));
        userDao.insert(user);
        System.out.println("Second query result: " + userDao.findById(id));
        userDao.insert(user);
    } catch (Exception e) {
        System.out.println("An exception occurred, performing manual rollback!");
        // Manually rollback the transaction
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        e.printStackTrace();
    }
    return false;
}

Example 3: Transaction with Sub-methods

To make a sub-method participate in the transaction, either use rollbackFor on the sub-method or have it throw an exception that the calling method handles. Note that the sub-method must also be public.

@Transactional
public boolean test3(User user) {
    try {
        System.out.println("First query result: " + userDao.findById(user.getId()));
        deal1(user);
        deal2(user);
        deal3(user);
    } catch (Exception e) {
        System.out.println("An exception occurred, performing manual rollback!");
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        e.printStackTrace();
    }
    return false;
}

public void deal1(User user) throws SQLException {
    userDao.insert(user);
    System.out.println("Second query result: " + userDao.findById(user.getId()));
}

public void deal2(User user) throws SQLException {
    if (user.getAge() < 20) {
        // This will cause an SQL exception (duplicate primary key)
        userDao.insert(user);
    } else {
        user.setAge(21);
        userDao.update(user);
        System.out.println("Third query result: " + userDao.findById(user.getId()));
    }
}

@Transactional(rollbackFor = SQLException.class)
public void deal3(User user) {
    if (user.getAge() > 20) {
        // This will cause an SQL exception (duplicate primary key)
        userDao.insert(user);
    }
}

Example 4: Programmatic Transaction Management

If you prefer not to use @Transactional, you can manage transactions programmatically using DataSourceTransactionManager and TransactionDefinition. Be careful to only rollback when a transaction is active and not yet committed.

@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;

public boolean test4(User user) {
    TransactionStatus transactionStatus = null;
    boolean committed = false;
    try {
        transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        System.out.println("First query result: " + userDao.findById(user.getId()));
        userDao.insert(user);
        System.out.println("Second query result: " + userDao.findById(user.getId()));

        if (user.getAge() < 20) {
            user.setAge(user.getAge() + 2);
            userDao.update(user);
            System.out.println("Third query result: " + userDao.findById(user.getId()));
        } else {
            throw new Exception("Simulating an exception!");
        }

        dataSourceTransactionManager.commit(transactionStatus);
        committed = true;
        System.out.println("Transaction committed successfully!");
        throw new Exception("Simulating a second exception!");

    } catch (Exception e) {
        if (!committed) {
            System.out.println("An exception occurred, performing manual rollback!");
            dataSourceTransactionManager.rollback(transactionStatus);
        }
        e.printStackTrace();
    }
    return false;
}

The examples above cover common use cases. Another approach is using savepoints:

Object savepoint = null;
try {
    // Set a savepoint
    savepoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
} catch (Exception e) {
    // Rollback to the savepoint
    TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savepoint);
}

Main Components

Entity Class

public class User {
    private Long id;
    private String name;
    private Integer age;
    // Getters and setters omitted
}

RestController

@RestController
@RequestMapping(value = "/api/user")
public class UserRestController {

    @Autowired
    private UserService userService;

    @Autowired
    private UserDao userDao;

    @PostMapping("/test1")
    public boolean test1(@RequestBody User user) {
        System.out.println("Request parameters: " + user);
        try {
            userService.test1(user);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Final query result: " + userDao.findById(user.getId()));
        return true;
    }

    @PostMapping("/test2")
    public boolean test2(@RequestBody User user) {
        System.out.println("Request parameters: " + user);
        userService.test2(user);
        System.out.println("Final query result: " + userDao.findById(user.getId()));
        return true;
    }

    @PostMapping("/test3")
    public boolean test3(@RequestBody User user) {
        System.out.println("Request parameters: " + user);
        userService.test3(user);
        System.out.println("Final query result: " + userDao.findById(user.getId()));
        return true;
    }

    @PostMapping("/test4")
    public boolean test4(@RequestBody User user) {
        System.out.println("Request parameters: " + user);
        userService.test4(user);
        System.out.println("Final query result: " + userDao.findById(user.getId()));
        return true;
    }
}

Main Application Class

@EnableTransactionManagement
@SpringBootApplication
public class TransactionalApp {

    public static void main(String[] args) {
        SpringApplication.run(TransactionalApp.class, args);
        System.out.println("Transactional application is running...");
    }
}

Testing

After starting the application, use Postman to test the endpoints. The examples below correspond to the code examples above.

Test 1: Basic @Transactional

With out @Transactional:

  • POST to http://localhost:8182/api/user/test1 with body {"id": 1, "name": "xuwujing", "age": 18}
  • Console output shows that the data is inserted despite the exception:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
Duplicate entry '1' for key 'PRIMARY'
Final query result: User [id=1, name=xuwujing, age=18]

With @Transactional:

  • After uncommenting @Transactional and deleting the previously inserted record, POST again.
  • Console output shows the data is rolled back:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
Duplicate entry '1' for key 'PRIMARY'
Final query result: null

Test 2: Manual Rollback

  • POST to http://localhost:8182/api/user/test2 with body {"id": 1, "name": "xuwujing", "age": 18}
  • Console output shows the transaction is rolled back:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
An exception occurred, performing manual rollback!
Duplicate entry '1' for key 'PRIMARY'
Final query result: null

Test 3: Sub-method Transaction

Test with age=18:

  • POST to http://localhost:8182/api/user/test3 with body {"id": 1, "name": "xuwujing", "age": 18}
  • Console output shows rollback:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
An exception occurred, performing manual rollback!
Duplicate entry '1' for key 'PRIMARY'
Final query result: null

Test with age=21:

  • POST with body {"id": 1, "name": "xuwujing", "age": 21}
  • Console output shows rollback after a successful update:
Request parameters: User [id=1, name=xuwujing, age=21]
First query result: null
Second query result: User [id=1, name=xuwujing, age=21]
Third query result: User [id=1, name=xuwujing2, age=21]
An exception occurred, performing manual rollback!
Duplicate entry '1' for key 'PRIMARY'
Final query result: null

Test 4: Programmatic Transaction

Test with age=18:

  • Delete the record with id=1 first.
  • POST to http://localhost:8182/api/user/test4 with body {"id": 1, "name": "xuwujing", "age": 18}
  • Console output shows commit followed by an exception:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
Third query result: User [id=1, name=xuwujing2, age=20]
Transaction committed successfully!
Simulating a second exception!
Final query result: User [id=1, name=xuwujing, age=20]

Test with age=21:

  • Delete the record with id=1 again.
  • POST with body {"id": 1, "name": "xuwujing", "age": 21}
  • Console output shows rolback:
Request parameters: User [id=1, name=xuwujing, age=21]
First query result: null
Second query result: User [id=1, name=xuwujing, age=21]
An exception occurred, performing manual rollback!
Simulating an exception!
Final query result: null

These tests confirm that the transactional behavior works as expected.

Additional Resources

Project Repository

Reference

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.