DEV Community

Wynn TEO
Wynn TEO

Posted on

Spring @Transactional Rollback Handling

Spring @Transactional Rollback Handling

Photo by [James Harrison](https://unsplash.com/@jstrippa?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/programming?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

By default, the spring boot transaction is auto-commit. Every single SQL statement is in its own transaction and will commit after execution. Take a look at the below example, the product is inserted into the database even though an exception has been raised.

public void createProduct() {  
    System.out.println("------ createProduct ------");
    Product prod = new Product();
    prod.setDescription("This is an example with runtime exception but no rollback.");
    prod.setPrice(10);
    prod.setTitle("First Product");
    productRepository.save(prod);
    System.out.println("First Product inserted.");

throw new RuntimeException();
}
Enter fullscreen mode Exit fullscreen mode

This shouldn’t be the case. In some scenarios, multiple transactions are run in a logical unit of code. All the transactions should only commit when there is no exception occurring in between.

In Spring Boot, when @Transactional annotation is used, Spring Boot implicitly creates a proxy that will be creating a connection to the database. A transaction will be started and committed after the code has been executed errorless. Otherwise, it will roll back the changes if an exception occurred.

**import** **java.sql.Connection**;

**Connection** conn = dataSource.getConnection(); 

**try** (connection) {

 *   // execute some SQL statements...*

 *   *// commit transaction
    conn.commit();

} **catch** (**SQLException** e) {
    conn.rollback();
}
Enter fullscreen mode Exit fullscreen mode

Example 1

In this example, we added @Transactional annotation to roll back the transaction if the exception occurred.

**@Transactional**
public void createProduct() {  
    System.out.println("------ createProduct ------");
    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);
    System.out.println("Second Product inserted.");

    throw new RuntimeException();
}
Enter fullscreen mode Exit fullscreen mode

The above example proves that the @Transactional annotation can roll back the transaction if the exception occurs. Take note, Spring only rolled back on unchecked exceptions by default.

Example 2a

This example shows that the checked exception will not be rolled back even though we have specified the **@Transactional **annotation.

@Transactional
public void createProduct() throws Exception{  
    System.out.println("------ createProduct ------");
    Product prod = new Product();
    prod.setDescription("This is an example with checked exception and transactional annotation.");
    prod.setPrice(10);
    prod.setTitle("Second Product");
    productRepository.save(prod);
    System.out.println("Second Product inserted.");
    throw new SQLException();
}
Enter fullscreen mode Exit fullscreen mode

Example 2b

To roll back checked exceptions, we need to specify the rollbackFor

@Transactional( **rollbackFor** = SQLException.class)
public void createProduct() throws Exception{  
    System.out.println("------ createProduct ------");
    Product prod = new Product();
    prod.setDescription("This is an example with checked exception and transactional annotation with rollbackFor.");
    prod.setPrice(10);
    prod.setTitle("Example 2b Product");
    productRepository.save(prod);
    System.out.println("Example 2b inserted.");
    throw new SQLException();
}
Enter fullscreen mode Exit fullscreen mode

We see that the transaction has rolled back successfully.

Example 3

In this example, we added the try-catch block to see if the rollback still working.

@Transactional
public void createProduct() {
    try {
        System.out.println("------ createProduct ------");
        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);
        System.out.println("Example 3 Product inserted.");
        throw new RuntimeException();
    }catch (Exception e){
        System.out.println("Here we catch the exception.");
    }
}
Enter fullscreen mode Exit fullscreen mode

It won’t work. This happened because we manually caught the exception and handle it. Therefore the current transaction is being executed normally and committed.

Example 4

What about a nested transaction? From the example below, the createProduct()*and *createOrder() methods will insert a record into the database. The createOrder() method has thrown a runtime exception. Both methods have been annotated with the **@Transactional **annotation.

//ProductController.java
@Transactional
public void createProduct() {
    System.out.println("------ createProduct ------");
    Product prod = new Product();
    prod.setDescription("This is createProduct method.");
    prod.setPrice(10);
    prod.setTitle("Create Product");
    productRepository.save(prod);
    orderController.createOrder();
}

//OrderController.java
@Transactional
public void createOrder() {
    System.out.println("------ createOrder ------");
    Order order = new Order();
    order.setTitle("Create Order");
    order.setDescription("This is createOrder method with runtime exception");
    orderRepository.save(order);  
    throw new RuntimeException("Create Order RuntimeException");
}
Enter fullscreen mode Exit fullscreen mode

createProduct()*and *createOrder() transactions were rolled back. Even though the runtime exception has happened in the createOrder() method. However, because it doesn’t handle the exception, this led to the createProduct() transaction also rollback.

Example 5

Let’s add in the try-and-catch block to handle the runtime exception in the createProduct() method.

//ProductController.java
@Transactional
public void createProduct() {
    System.out.println("------ createProduct ------");
    Product prod = new Product();
    prod.setDescription("This is createProduct method.");
    prod.setPrice(10);
    prod.setTitle("Create Product");
    productRepository.save(prod);
    try{
        orderController.createOrder();
    }catch (RuntimeException e){
        System.out.println("Handle " + e.getMessage());
    }
}

//OrderController.java
@Transactional
public void createOrder() {
    System.out.println("------ createOrder ------");
    Order order = new Order();
    order.setTitle("Create Order");
    order.setDescription("This is createOrder method with runtime exception");
    orderRepository.save(order);  
    throw new RuntimeException("Create Order RuntimeException");
}
Enter fullscreen mode Exit fullscreen mode

Arg? Both records have been rollback. Why? At the same time, we also noticed the exception error.

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
Enter fullscreen mode Exit fullscreen mode

@Transactional has a parameter called **Propagation. **Propagation will define the transaction boundary. Spring will start and end the transaction according to the propagation setting. By default, propagation is set to **REQUIRED. **So, if there is an active transaction, Spring will consume it instead of creating a new transaction. Therefore, in the above scenario, even though we have caught the exception on the outer method, the transaction was still rolling back.

Example 6

Use Propagation **REQUIRES_NEW, **this will force spring to create a subtransaction.

@Transactional
public void createProduct() {
    System.out.println("------ createProduct ------");
    Product prod = new Product();
    prod.setDescription("This is createProduct method.");
    prod.setPrice(10);
    prod.setTitle("Create Product");
    productRepository.save(prod);
    try{
        orderController.createOrder();
    }catch (RuntimeException e){
        System.out.println("Handle " + e.getMessage());
    }
}

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void createOrder() {
    System.out.println("------ createOrder ------");
    Order order = new Order();
    order.setTitle("Create Order");
    order.setDescription("This is createOrder method with runtime exception");
    orderRepository.save(order);  
    throw new RuntimeException("Create Order RuntimeException");
}
Enter fullscreen mode Exit fullscreen mode

The product record was written into the database, and the order record was rolled back.

Example 7

What if we never use try and catch block in the outer method, and an exception has happened in the inner method? Will the outer transaction still commit?

@Transactional
public void createProduct() {
    System.out.println("------ createProduct ------");
    Product prod = new Product();
    prod.setDescription("This is createProduct method.");
    prod.setPrice(10);
    prod.setTitle("Create Product");
    productRepository.save(prod);
    orderController.createOrder();
}

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void createOrder() {
    System.out.println("------ createOrder ------");
    Order order = new Order();
    order.setTitle("Create Order");
    order.setDescription("This is createOrder method with runtime exception");
    orderRepository.save(order);  
    throw new RuntimeException("Create Order RuntimeException");
}
Enter fullscreen mode Exit fullscreen mode

We see that both transactions have been rolled back. Because the inner transaction threw an exception, the outer transaction detected the exception and it hasn’t been handled. Therefore, the outer transaction has been rolled back.

Example 8

Let’s see if the exception has happened in the outer method instead.

@Transactional
public void createProduct() {
    System.out.println("------ createProduct ------");
    Product prod = new Product();
    prod.setDescription("This is createProduct method.");
    prod.setPrice(10);
    prod.setTitle("Create Product with runtime");
    productRepository.save(prod);
    orderController.createOrder();
    throw new RuntimeException("Create Product RuntimeException");
}

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void createOrder() {
    System.out.println("------ createOrder ------");
    Order order = new Order();
    order.setTitle("Create Order with propagation required_new");
    order.setDescription("This is createOrder method.");
    orderRepository.save(order);  
}
Enter fullscreen mode Exit fullscreen mode

We see that the inner transaction has been committed successfully due to the Propagation.REQUIRES_NEW. This forces the Spring to handle the query in subtransaction. Once it is completed the outer transaction resumed, and an exception happened at this moment, so it rolled back.

Additional Notes

To take note, if both methods are in the same class, the @Transactional annotation will not take place. It will always be treated as one transaction, so even if we specified the Propagation.REQUIRES_NEW, it won’t work. At the very beginning, we talked about Spring creating a proxy when seeing annotation @Transactional. When you are calling the internal method, it will bypass the proxy.

Thanks for reading!

Top comments (1)

Collapse
 
renciac profile image
Rencia Cloete

Nice