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
}
}