Java Spring Boot Transaction Management
Introduction
In Spring applications, effective transaction management is essential to maintain data integrity during database operations. A transaction guarantees that a series of database actions either fully succeed or fail, adhering to the principles of atomicity, consistency, isolation, and durability (ACID). This comprehensive guide navigates through various transaction scenarios, behaviours, and best practices within Java Spring Transaction Management.
Spring Transaction Management
By default, Spring Boot operates in auto-commit mode for transactions, where each SQL statement is treated as its own transaction and is automatically committed. This example illustrates the default behaviour:
public void createProduct() {
Product prod = new Product();
prod.setDescription("This is an example with runtime exception but no transactional.");
prod.setPrice(10);
prod.setTitle("First Product");
productRepository.save(prod);
throw new RuntimeException();
}
In this code, an exception is raised, yet it results in the successful insertion of the product into the database.
Example 1: Basic Usage of @Transactional
The @Transactional
annotation in Spring Boot implicitly creates a proxy that initiates a transaction and commits it if no errors occur. When an exception arises, it ensures that the changes are rolled back. In this example, the transaction is rolled back when a RuntimeException
occurs, maintaining data consistency:
@Transactional
public void createProductWithTransactional() {
Product prod = new Product();
prod.setDescription("This is an example with runtime exception and transactional annotation.");
prod.setPrice(10);
prod.setTitle("Second Product");
productRepository.save(prod);
throw new RuntimeException();
}
Handling Checked and Unchecked Exceptions
Spring Boot treats checked and unchecked exceptions differently by default. We can control this behavior using the rollbackFor
and noRollbackFor
attributes of the @Transactional
annotation.
Example 2a: Checked Exception Without Rollback
In this example, the checked exception (e.g., SQLException
) will not trigger a rollback because it’s a checked exception:
@Transactional
public void createProductWithCheckedException() throws Exception {
Product prod = new Product();
prod.setDescription("This is an example with a checked exception and a transactional annotation.");
prod.setPrice(10);
prod.setTitle("Example 1a Product");
productRepository.save(prod);
throw new SQLException();
}
Example 2b: Using rollbackFor
to Roll Back for Checked Exceptions
To roll back on checked exceptions, specify the rollbackFor
attribute in the @Transactional
annotation:
@Transactional(rollbackFor = SQLException.class)
public void createProductWithCheckedExceptionAndRollBackFor() throws Exception {
Product prod = new Product();
prod.setDescription("This is an example with a checked exception and a transactional annotation with rollbackFor.");
prod.setPrice(10);
prod.setTitle("Example 2b Product");
productRepository.save(prod);
throw new SQLException();
}
Example 2c: Using noRollbackFor
to Prevent Rollback for Specific Exceptions
Conversely, we may want to roll back for all exceptions except specific ones. Use the noRollbackFor
attribute to achieve this:
@Transactional(noRollbackFor = RuntimeException.class)
public void createProductWithRuntimeExceptionAndNoRollBackFor() {
Product prod = new Product();
prod.setDescription(
"This is an example with runtime exception, transactional annotation and noRollbackFor."
);
prod.setPrice(10);
prod.setTitle("Example 2c Product");
productRepository.save(prod);
throw new RuntimeException();
}
Transactions and Try-Catch Blocks
This section explores how try-catch blocks interact with transactions in Spring Boot.
Example 3: Using Try-Catch Block Inside @Transactional
In this scenario, the try-catch block inside the method catches the RuntimeException
. Since the exception is handled within the method, the transaction proceeds normally and gets committed:
@Transactional
public void createProduct() {
try {
Product prod = new Product();
prod.setDescription("This is an example with runtime exception, transactional annotation and try catch.");
prod.setPrice(10);
prod.setTitle("Example 3 Product");
productRepository.save(prod);
throw new RuntimeException();
} catch (Exception e) {
System.out.println("Here we catch the exception.");
}
}
Controlled Transactions with Propagation
Spring transactions allow us to control transaction behavior using propagation settings.
Example 4a: Transaction Behavior with Propagation.REQUIRED
In this example, both createProduct()
and createOrder()
methods are annotated with @Transactional
. The createProduct()
method calls the createOrder()
method, and a RuntimeException
is thrown inside createOrder()
.
// ProductService.java
@Transactional
public void createProductWithInnerCallingException() {
Product prod = new Product();
prod.setDescription(
"This is an example runtime exception in inner method calling and transactional annotation."
);
prod.setPrice(10);
prod.setTitle("Example 4a Product");
productRepository.save(prod);
this.orderService.createOrder();
}
// OrderService.java
@Transactional
public void createOrder() {
Order order = new Order();
order.setTitle("Example 4a Order");
order.setDescription(
"This is create order with runtime exception and transactional annotation"
);
orderRepository.save(order);
throw new RuntimeException("Create Order RuntimeException");
}
In this example, both createProduct()
and createOrder()
transactions are rolled back if a runtime exception occurs in the createOrder()
method. This is due to the default Propagation.REQUIRED
behavior.
By default, the @Transactional
annotation uses a propagation behavior called Propagation.REQUIRED
. This means that if there is an active transaction, Spring will join that transaction instead of creating a new one.
In this example, when createOrder()
is called from createProduct()
, it joins the existing transaction started by createProduct()
.
Since a RuntimeException is thrown inside createOrder()
, and it’s not caught within the method, the exception propagates to the calling method createProduct()
. The exception is not handled by outer method as well, so the entire transaction is marked for rollback.
This ensures the atomicity of the transaction, meaning that all changes are either committed or rolled back together.
Example 4b: Transaction Behavior with Try-Catch in Propagation.REQUIRED
In this example, the createProduct()
method calls the createOrder()
method, where a RuntimeException
is thrown. The createProduct()
method catches this exception using a try-and-catch block.
// ProductService.java
@Transactional
public void createProductWithInnerCallingExceptionAndTryCatch() {
try {
Product prod = new Product();
prod.setDescription(
"This is an example runtime exception in inner method calling and transactional annotation."
);
prod.setPrice(10);
prod.setTitle("Example 4b Product");
productRepository.save(prod);
this.orderService.createOrder();
} catch (Exception e) {
System.out.println("Here we catch the runtime exception.");
}
}
// OrderService.java
@Transactional
public void createOrder() {
Order order = new Order();
order.setTitle("Example 4b Order");
order.setDescription(
"This is create order with runtime exception and transactional annotation"
);
orderRepository.save(order);
throw new RuntimeException("Create Order RuntimeException");
}
In this case, even though the exception is caught, both transactions are rolled back because the transaction is already marked for rollback before the catch block is executed.
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
The exception org.springframework.transaction.UnexpectedRollbackException
indicates that the transaction was silently rolled back because it was marked as rollback-only. The behaviour ensures the atomicity of the transaction. If one part of the transaction fails, the entire transaction is rolled back, maintaining the consistency of the system.
Example 4c: Transaction Behavior with Try-Catch in Propagation.NEW
Using Propagation.REQUIRES_NEW
forces Spring to create a subtransaction, allowing the outer transaction to commit even if the inner transaction rolls back. In this example, the product record is written into the database, and the order record is rolled back:
@Transactional
public void createProductWithInnerCallingExceptionTryCatchAndPropagationNew() {
try {
Product prod = new Product();
prod.setDescription(
"This is an example runtime exception and propagation NEW in inner method calling and transactional annotation."
);
prod.setPrice(10);
prod.setTitle("Example 4c Product");
productRepository.save(prod);
this.orderService.createOrderWithPropagationNEW();
} catch (Exception e) {
System.out.println("Here we catch the runtime exception.");
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderWithPropagationNEW() {
Order order = new Order();
order.setTitle("Example 4c Order");
order.setDescription(
"This is create order with runtime exception, transactional annotation and propagation NEW"
);
orderRepository.save(order);
throw new RuntimeException("Create Order RuntimeException");
}
Example 4d: Transaction Behavior without Try-Catch in Propagation.NEW
When an exception occurs in the inner method createOrder()
, and it’s not caught, both transactions are rolled back. The inner transaction marked for rollback also affects the outer transaction.
@Transactional
public void createProductWithInnerCallingExceptionPropagationNew() {
Product prod = new Product();
prod.setDescription(
"This is an example runtime exception and propagation NEW in inner method calling, outer method no try catch and transactional annotation."
);
prod.setPrice(10);
prod.setTitle("Example 4d Product");
productRepository.save(prod);
this.orderService.createOrderWithExceptionPropagationNEW();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderWithExceptionPropagationNEW() {
Order order = new Order();
order.setTitle("Example 4d Order");
order.setDescription(
"This is create order with runtime exception, transactional annotation and propagation NEW"
);
orderRepository.save(order);
throw new RuntimeException("Create Order RuntimeException");
}
Example 4e: Transaction Behavior with Exception in Outer Method for Propagation.NEW
In this case, the exception is thrown in the outer method createProduct()
after the inner method createOrder()
has completed successfully. This results in the outer transaction being rolled back, but the inner transaction is not affected.
@Transactional
public void createProductWithInnerCallingPropagationNewOuterException() {
Product prod = new Product();
prod.setDescription(
"This is an example with transactional annotation, outer method exception and inner calling with propagation NEW."
);
prod.setPrice(10);
prod.setTitle("Example 4e Product");
productRepository.save(prod);
this.orderService.createOrderPropagationNEWWithoutException();
throw new RuntimeException("Create Product RuntimeException");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderPropagationNEWWithoutException() {
Order order = new Order();
order.setTitle("Example 4e Order");
order.setDescription(
"This is create order with transactional annotation and propagation NEW"
);
orderRepository.save(order);
}
It’s essential to note that if both methods are in the same class, the @Transactional
annotation will not create a new transaction, even with Propagation.REQUIRES_NEW
. This is because the internal method call will bypass the proxy created by Spring, and the propagation setting will not take effect.
Notes: However, it’s essential to note that if both methods are in the same class, the @Transactional
annotation will not create a new transaction, even with Propagation.REQUIRES_NEW
. This is because the internal method call will bypass the proxy created by Spring, and the propagation setting will not take effect.
Conclusion
In this tutorial, we explored the fundamental concepts of transactions in Spring and how to handle exceptions within these transactions. We began by understanding the basic usage of the @Transactional
annotation, which automatically starts and commits transactions, rolling them back when exceptions occur. We dive into the differentiation between checked and unchecked exceptions, exploring how to control rollback behavior using attributes like rollbackFor
and noRollbackFor
.
In the later part, we examined the interaction between transactions and try-catch blocks. Furthermore, we explored the use of Propagation.REQUIRES_NEW
to create new transactions and how the behavior of inner and outer transactions can be controlled independently.
Share this content:
Leave a Comment