Transactions in Spring 6

Dependencies

To work with transactions in Spring 6, include the necessary dependencies:

<dependencies>
    <!-- Spring JDBC (persistence layer support) -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>6.0.2</version>
    </dependency>
    <!-- MySQL Connector -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>
    <!-- Druid Data Source -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.15</version>
    </dependency>
</dependencies>

Testing CRUD Operations with JdbcTemplate

The JdbcTemplate simplifies database operations. Here’s a test class for insert operations:

package com.example.spring6.tx;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = "classpath:spring-beans.xml")
public class JdbcCrudTest {

    @Autowired
    private JdbcTemplate dbTemplate;

    @Test
    public void testInsert() {
        String sql = "INSERT INTO employee (id, name, age, gender) VALUES (NULL, ?, ?, ?)";
        int rows = dbTemplate.update(sql, "Bob", 25, "Male");
        System.out.println("Inserted rows: " + rows);
    }
}

Querying Data

Retrieve single objects, lists, or scalar values:

package com.example.spring6.tx;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import java.util.List;

@SpringJUnitConfig(locations = "classpath:spring-beans.xml")
public class JdbcQueryTest {

    @Autowired
    private JdbcTemplate dbTemplate;

    // Retrieve a single entity
    @Test
    public void testQuerySingle() {
        String sql = "SELECT * FROM employee WHERE id = ?";
        Employee emp = dbTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Employee.class), 1);
        System.out.println(emp);
    }

    // Retrieve a list of entities
    @Test
    public void testQueryList() {
        String sql = "SELECT * FROM employee";
        List<Employee> employees = dbTemplate.query(sql, new BeanPropertyRowMapper<>(Employee.class));
        employees.forEach(System.out::println);
    }

    // Retrieve a scalar value (e.g., count)
    @Test
    public void testQueryCount() {
        String sql = "SELECT COUNT(*) FROM employee";
        Integer count = dbTemplate.queryForObject(sql, Integer.class);
        System.out.println("Total employees: " + count);
    }
}

class Employee {
    private Integer id;
    private String name;
    private Integer age;
    private String gender;

    // Getters and setters
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public String getGender() { return gender; }
    public void setGender(String gender) { this.gender = gender; }

    @Override
    public String toString() {
        return "Employee{id=" + id + ", name='" + name + "', age=" + age + ", gender='" + gender + "'}";
    }
}

Understanding Database Transactions

A transaction is a unit of work that either completes entirely or fails (rolls back). It has four core properties:

Atomicity (A)

All operations in a transaction succeed or fail together. If an error occurs, changes are rolled back to the original state.

Consistency (C)

The database remains valid before and after the transaction. For example, a transfer between accounts preserves the total balance.

Isolation (I)

Concurrent transactions do not interfere. A transaction sees data as it was before or after another transaction, not in an intermediate state.

Durability (D)

Committed changes persist even after system failures (e.g., crashes or restarts).

Using @Transactional Annotation

Apply @Transactional to methods or classes to manage transacsions. Typically, transactions are managed at the service layer.

Placing @Transactional

  • Method: Only the method is transactional.
  • Class: All methods in the class are transactional.
package com.example.spring6.tx.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BookService {

    @Transactional
    public void purchaseBook(int bookId, int userId) {
        // Business logic (e.g., update inventory, debit user account)
    }
}

Transaction Attributes

Read-only

Marks a transaction as read-only (throws an error if data is modified):

@Transactional(readOnly = true)
public List<Book> listBooks() {
    // Read-only operation (modifications throw exceptions)
}

Timeout

Rolls back if the transaction exceeds the timeout (in seconds):

@Transactional(timeout = 5)
public void processOrder() {
    // Long-running operation (rolls back if not completed in 5s)
}

Rollback Strategy

Specifies exceptions that do not trigger rollback:

@Transactional(noRollbackFor = ArithmeticException.class)
public void calculateDiscount() {
    // Arithmetic exceptions won’t roll back the transaction
}

Isolation Levels

Defines how transactions interact with concurrent operations (e.g., READ_COMMITTED, REPEATABLE_READ):

@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateAccount() {
    // Isolation level: read committed (default for many databases)
}

Propagation Behavier

Controls how transactions are nested or started (e.g., REQUIRED, REQUIRES_NEW):

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void initiatePayment() {
    // Start a new transaction (suspends existing one)
}

Configuration

Annotation-driven Configuration

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="...">

    <context:component-scan base-package="com.example.spring6.tx" />
    <context:property-placeholder location="classpath:jdbc.properties" />

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}" />
        <property name="driverClassName" value="${jdbc.driver}" />
        <property name="username" value="${jdbc.user}" />
        <property name="password" value="${jdbc.password}" />
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

XML-based Configuration

For legacy setups, use XML to configure transactions:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="...">

    <context:component-scan base-package="com.example.spring6.xmltx" />
    <context:property-placeholder location="classpath:jdbc.properties" />

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}" />
        <property name="driverClassName" value="${jdbc.driver}" />
        <property name="username" value="${jdbc.user}" />
        <property name="password" value="${jdbc.password}" />
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true" />
            <tx:method name="update*" propagation="REQUIRED" />
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="serviceMethods" expression="execution(* com.example.spring6.xmltx.service.*.*(..))" />
        <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods" />
    </aop:config>
</beans>

Testing Transactions

Test transactional behavior using SpringJUnitConfig:

package com.example.spring6.tx;

import com.example.spring6.tx.service.BookService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = "classpath:spring-beans.xml")
public class TransactionTest {

    @Autowired
    private BookService bookService;

    @Test
    public void testPurchase() {
        bookService.purchaseBook(101, 201); // Test transactional logic
    }
}

Tags: Spring 6 transactions java Spring JDBC ACID

Posted on Thu, 27 Aug 2026 16:33:18 +0000 by anibiswas