The labs on this site need JavaScript to run. The same content is written out below.
Fundamentals › Spring
What does @Transactional group together?
@Transactional is a marker (an annotation) that you put above a method. The order() method in this lab saves to the database twice with jdbc.update (an order in the orders table and a payment in the payment table). Between the two saves there is a stock check, and if stock is 0, it throws an exception. With no marker, the database applies each save as it receives it, so even if an exception occurs, the earlier save stays. If you put @Transactional above order(), Spring treats the saves inside that method as one unit (a transaction). If the method ends normally, it applies all of them (commit), and if it ends with a runtime exception, it cancels all of them (rollback). We do not write the code that starts the unit and applies or cancels it. Spring does it before and after it calls order(). In this lab, you remove @Transactional or change the value put in stock, call the same order(), and look at the rows left in the two tables after the call ends.
Checked against real execution · Ran all 4 combinations with a real @Transactional proxy on Spring 7.0.9 and H2 2.4.240, and checked the row counts and the facts behind the step sentences
@Service
public class OrderService {
private final JdbcTemplate jdbc;
public OrderService(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
// Unit: the saves inside this method are all applied or all canceled
@Transactional
public void order() {
jdbc.update("INSERT INTO orders VALUES (1, 'keyboard')");
int stock = 0;
if (stock == 0) {
throw new IllegalStateException("Out of stock");
}
jdbc.update("INSERT INTO payment VALUES (1, 30000)");
}
}
This was checked against real execution of Spring Framework 7.0.9 on JDK 21, connected by JDBC to H2 2.4.240 (an in-memory database), and it was not checked against execution on MySQL. For all 4 combinations, the same source as the OrderService on the screen (with only the package and import lines and two measurement lines marked PROBE at the end added) was compiled, and a new Java process was started for each combination. The configuration class had @EnableTransactionManagement, which AnnotationConfigApplicationContext reads, an H2 DataSource (HikariCP), JdbcTemplate, and DataSourceTransactionManager (a subclass with only lines added to count starts, commits, and rollbacks), and OrderService was marked @Service so that Spring finds it by component scanning. Auto-commit was left at the JDBC default (on), and the database and the two tables (orders, payment) were created anew for each combination. What was checked is how order() ended (IllegalStateException and its message, or normal) and the number of rows in orders and payment counted by a separate query after the call ended. The facts that the step sentences rely on were also checked by execution: whether a transaction is active inside order() (active only when there is @Transactional), the class of the OrderService bean that main received (a CGLIB proxy class made by Spring when there is @Transactional, and the class we wrote as it is when there is not), the number of rows in orders counted over a separately connected database connection right after the first save (0 rows with @Transactional, 1 row without it), and the number of times the transaction manager started, committed, and rolled back (1 start and 1 rollback if it ended with an exception, 1 start and 1 commit if it ended normally, and all 0 without @Transactional). That the second save is not final inside the transaction, and that it is applied immediately without one, were derived from the value measured after the first save and from the final row counts, and the moment right after the second save was not measured separately. What the documentation defines is the Declarative Transaction Management section of Data Access in the Spring Framework 7.0 documentation (that declarative transactions are done through AOP), the section Understanding the Spring Framework's Declarative Transaction Implementation (that TransactionInterceptor and the transaction manager drive the transaction around the method call), the section Using @Transactional (that it must be turned on with @EnableTransactionManagement and that the default is the proxy mode, and that by default RuntimeException and Error are rolled back while checked exceptions are not), and the section Rolling Back a Declarative Transaction (that in the default settings only runtime exceptions are marked for rollback). However, the documentation does not describe in one paragraph the order of starting before the method, committing if normal, and rolling back if an exception, so that order is an observation based on the counts and the row numbers that the Probe measured. The code on the screen leaves out the package and import lines, and it does not show the main that calls order(), the CREATE TABLE that creates the tables, the database connection, and the Spring configuration. This lab covers only one exception, a RuntimeException being thrown, and does not cover checked exceptions, calls from inside the same class, or readOnly. Checked exceptions, rollbackFor, and propagation are covered in the "Transaction propagation" lab, and transactions themselves (including isolation levels) are covered in the "Transaction basics" lab.
Previous lab: JDBC · Next lab: Spring Boot
Struck-through text was removed from the first version of the code; highlighted text was added.