Spring - Rollback with @Transactional Annotation

[Updated: Sep 12, 2017, Created: Sep 12, 2017]

In the last tutorial, we saw how to implement declarative transactions with @Transactional annotation. With this annotation, any method which throws a unchecked exception (RuntimeException or Error and subclasses) will trigger the rollback automatically, but any check Exception will not trigger the rollback unless we specify 'rollbackFor' attribute of @Transactional.

Example

We are going to reuse our previous example. We will create a custom check exception InvalidOrderItemException and specify 'rollbackFor' attribute with @Transactional annotation.

Note that in previous example, the transaction was rolled back itself because of the DataIntegrityViolationException which is a Spring's runtime exception. Spring wraps many database driver's exceptions, around unchecked runtime exceptions. In the last example original root cause exception was org.h2.jdbc.JdbcSQLException which is a check exception.

The Service with @Transactional

package com.logicbig.example;

import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Transactional
public interface OrderService {

  void persistOrders(List<OrderItem> orderItems) throws InvalidOrderItemException;

  List<OrderItem> getAllOrders();
}
@Service
public class OrderServiceImpl implements OrderService {

  @Autowired
  private Dao<OrderItem> dao;

  @Override
  @Transactional(rollbackFor = InvalidOrderItemException.class)
  public void persistOrders(List<OrderItem> orderItems) throws InvalidOrderItemException {
      for (OrderItem orderItem : orderItems) {
          if (orderItem.getQty() > 100) {
              throw new InvalidOrderItemException(
                      "Order quantity cannot be more than 100, found: "
                              + orderItem.getQty());
          }
          long id = dao.save(orderItem);
          System.out.println("id generated: " + id);
      }
  }

  @Override
  public List<OrderItem> getAllOrders() {
      return dao.loadAll();
  }
}
public class InvalidOrderItemException extends Exception {
  public InvalidOrderItemException(String message) {
      super(message);
  }
}

Using the service

@Component
public class OrderItemClientBean {
  @Autowired
  private OrderService orderService;

  public void persistOrderItems() {
      List<OrderItem> orders = Arrays.asList(
              new OrderItem("BWell Ethernet Cable", 5),
              new OrderItem("EDrive SSD", 2000)
      );
      try {
          orderService.persistOrders(orders);
      } catch (InvalidOrderItemException e) {
          logException(e);
      }
      List<OrderItem> allOrders = orderService.getAllOrders();
      System.out.println("loaded orders: " + allOrders);

      System.out.println("-- second attempt --");
      List<OrderItem> orders2 = Arrays.asList(
              new OrderItem("BWell Ethernet Cable", 5),
              new OrderItem("EDrive SSD", 20)
      );
      try {
          orderService.persistOrders(orders2);
      } catch (InvalidOrderItemException e) {
          logException(e);
      }
      List<OrderItem> allOrders2 = orderService.getAllOrders();
      System.out.println("loaded orders: " + allOrders2);
  }

  private static void logException(Exception e) {
      System.out.println("-- exception --");
      System.err.println("Exception: "+e.getClass().getName());
      System.err.println("Exception Message: "+e.getMessage());
      System.out.println("---------");
  }
}
public class ExampleMain {
  public static void main(String[] args) {
      AnnotationConfigApplicationContext context =
              new AnnotationConfigApplicationContext(AppConfig.class);
      OrderItemClientBean orderItemClientBean = context.getBean(OrderItemClientBean.class);
      orderItemClientBean.persistOrderItems();
  }
}

Output

id generated: 1
-- exception --
Exception: com.logicbig.example.InvalidOrderItemException
Exception Message: Order quantity cannot be more than 100, found: 2000
---------
loaded orders: []
-- second attempt --
id generated: 2
id generated: 3
loaded orders: [OrderItem{id=2, item='BWell Ethernet Cable', qty=5}, OrderItem{id=3, item='EDrive SSD', qty=20}]

Without 'rollbackFor'

In our example, if we don't specify 'rollbackFor' attribute:

@Service
public class OrderServiceImpl implements OrderService {
  ....
  @Override
  @Transactional
  public void persistOrders(List<OrderItem> orderItems) throws InvalidOrderItemException {
      for (OrderItem orderItem : orderItems) {
          if (orderItem.getQty() > 100) {
              throw new InvalidOrderItemException(
                      "Order quantity cannot be more than 100, found: "
                              + orderItem.getQty());
          }
          long id = dao.save(orderItem);
          System.out.println("id generated: " + id);
      }
  }
 .......
}

output

id generated: 1
-- exception --
Exception: com.logicbig.example.InvalidOrderItemException
Exception Message: Order quantity cannot be more than 100, found: 2000
---------
loaded orders: [OrderItem{id=1, item='BWell Ethernet Cable', qty=5}]
-- second attempt --
id generated: 2
id generated: 3
loaded orders: [OrderItem{id=1, item='BWell Ethernet Cable', qty=5}, OrderItem{id=2, item='BWell Ethernet Cable', qty=5}, OrderItem{id=3, item='EDrive SSD', qty=20}]

As seen in above output, even though there was exception with the first transaction, it was not rolled back and one to the two orders was still persisted. This shows that for checked exception transactions do not rollback implicitly unless we specify 'rollbackFor' attribute of @Transactional.

Example Project

Dependencies and Technologies Used :

  • spring-context 4.3.10.RELEASE: Spring Context.
  • spring-jdbc 4.3.10.RELEASE: Spring JDBC.
  • h2 1.4.196: H2 Database Engine.
  • JDK 1.8
  • Maven 3.3.9

@Transactional with rollbackFor Example Select All Download
  • transactional-roll-back
    • src
      • main
        • java
          • com
            • logicbig
              • example
        • resources

See Also