Spring @Transactional Annotation
Before we talk about the Spring annoatation @Transactional
, what exactly transaction
is? Transaction represents a single unit of work which can be described by the ACID properties. ACID stands for Atomicity
, Consistency
, Isolation
and Durability
:
Atomicity: Operations performed on the data are either all successful or one.
Consistency: The data remains consistent state before and after the transaction.
Isolation: Transactions are isolated to each other. Check my previous article here about the transaction isolation in PostgreSQL.
**Durability: ** Successful commits should persist the data even in the event of a system error.
JDBC Transaction Management #
In Java, the Java Database Connectivity (JDBC) API provides transaction support from the java.sql.Connection
interface. It provides the following three methods to perform transaction management.
- setAutoCommit()
- commit()
- rollback()
By default, a JDBC connection is created in auto-commit mode. It means each sql statement is implicitly demarcated with a transaction. Hence, we need to manage transaction manually (by setting auto-commit false) if a particular service involves multiple sql statements.
JDBC Transaction Management Example:
mport java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class SimpleJDBCExample {
public static void main(String[] args) {
String url = "jdbc:postgresql://localhost/postgres";
Properties props = new Properties();
props.setProperty("user", "caizhenhua");
props.setProperty("password", "123456");
try (Connection conn = DriverManager.getConnection(url, props)) {
try {
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
String insertSql1 = "insert into t_user (id, username, password) values (33, 'test', '321')";
String insertSql2 = "insert into t_user (id, username, password) values (33, 'test', '321')";
stmt.executeUpdate(insertSql1);
stmt.executeUpdate(insertSql2);
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
In the above example, no records are inserted because of duplicate key value violation. However, if auto-commit is enabled, the first update statement will success (data saved in database) while the second update statement throws exception. Let's see how spring @Transactional annotation ease the transaction management for developers.
Spring @Transactional Example #
This service
class is basically equivalent to the above example except it is much cleaner and elegant.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceWithTx {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void insertUser() {
System.out.println("UserService#UserService begin");
int i1 = jdbcTemplate.update("insert into t_user (id, username, password) values (37, 'test', '321')");
int i2 = jdbcTemplate.update("insert into t_user (id, username, password) values (38, 'test', '321')");
System.out.println("i1= " + i1);
System.out.println("i2= " + i2);
System.out.println("UserService#UserService end");
}
}
Driver class using the spring CommandLineRunner
interface.
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class SpringTxRunner implements CommandLineRunner {
private final UserServiceWithTx userService;
// Setter Injection
public SpringTxRunner(UserServiceWithTx userService) {
this.userService = userService;
}
@Override
public void run(String... args) throws Exception {
System.out.println("SpringTxRunner#run begin");
userService.insertUser();
System.out.println("SpringTxRunner#run end");
}
}
Basics of @Transactional #
The meta-anotations of @Transactional
are defined as follows:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
//...
}
- The
@Transactional
annotation can be annotated onTYPE
andMETHOD
. According to thejava.lang.annotation.ElementType
enum,TYPE
can be class, interface, annotation or record (Java 16). - Retention policy for @Transactional annotation is
RUNTIME
that means it can be accessed via reflection at runtime. - The
@Inherited
meta-annotation specifies that the annotation can be inherited by subclasses. - JavaDoc generates annotation info to all anotated classes if that annotation is marked with @Documented meta-annotation
@Transactional has total twelve optional elements, below is a simple example to illustrate all of them:
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Transactional(
isolation = Isolation.DEFAULT,
label = {"label1, label2"},
noRollbackFor = {RuntimeException.class, NullPointerException.class},
noRollbackForClassName = {"java.lang.RuntimeException", "java.lang.NullPointerException"},
propagation = Propagation.REQUIRED,
readOnly = true,
rollbackFor = {IllegalStateException.class},
rollbackForClassName = {"java.lang.IllegalStateException"},
timeout = 5,
timeoutString = "5",
transactionManager = "exampleTransactionManager",
value = "exampleTransactionManager"
)
public class AllTransactionalElements {
}
As you can see, it is not that hard to configure the @Transactional
annotation. But some properties should be carefully used for example the propagation
, lable
, transactionalManager
or value
. Overall, the benefits of Spring @Transactional are manifold:
- greatly reduce the boilerplate codes like
setAutoCommit(false)
- automatic rollback or no rollback in case of exceptions
- flexible transaction propagation