Search code examples
springspring-bootjpaspring-transactions

Spring transactions - Exception in new transaction cause rollback in parent one


I'm struggling with weird behaviour of method annotated with Propagation.REQUIRES_NEW.

In body of method placeOrder() the last operation is sending sms which could throws runtime exception.

I don't mind that sending sms will throw exception, so I don't want to have rollback of transaction started in placeOrder().

Method sendSms() has propagation REQUIRES_NEW so from what I understand, it should not trigger rollback from suspended transaction, only

@Component
public class OrderService {

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private ProductDao productDao;

    @Autowired
    private WarehouseService warehouseService;

    @Autowired
    private SmsClient smsClient;

    @Autowired
    private UserDao userDao;

    @Transactional
    @Override
    public void placeOrder(PlaceOrderRequest placeOrderRequest) {
        Product product = productDao.getById(placeOrderRequest.getProductId());
        WarehouseItem warehouseItem = warehouseService.getAvailable(product);
        Order order = orderDao.createNew();
        order.setProduct(product);
        order.setUser(user);
        smsClient.sendSms();
    }
}

@Component
public class SmsClient {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendSms() {
        throwEx();
    }

    private void throwEx() throws SmsClientException {
        throw new SmsClientException();
    }
}

Here are logs from TransactionManager:

Creating new transaction with name [ai.optime.springmicroservicetemplate.domain.order.impl.DefaultOrderService.placeOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Opened new EntityManager [SessionImpl(1447696365<open>)] for JPA transaction
Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@25711903]
Executing DAO method 'getById' from class 'JpaBasedProductDao' with args {1}
Found thread-bound EntityManager [SessionImpl(1447696365<open>)] for JPA transaction
Participating in existing transaction
Executing DAO method 'getAvailable' from class 'JpaBasedWarehouseItemDao' with args {1}
HHH000397: Using ASTQueryTranslatorFactory
Found thread-bound EntityManager [SessionImpl(1447696365<open>)] for JPA transaction
Participating in existing transaction
Executing DAO method 'createNew' from class 'JpaBasedOrderDao' with args {}
Found thread-bound EntityManager [SessionImpl(1447696365<open>)] for JPA transaction
Participating in existing transaction
Executing DAO method 'getById' from class 'JpaBasedUserDao' with args {1}
Found thread-bound EntityManager [SessionImpl(1447696365<open>)] for JPA transaction
Participating in existing transaction
Found thread-bound EntityManager [SessionImpl(1447696365<open>)] for JPA transaction
Suspending current transaction, creating new transaction with name [ai.optime.springmicroservicetemplate.api.sms.SmsClient.sendSms]
Opened new EntityManager [SessionImpl(1649890926<open>)] for JPA transaction
Exposing JPA transaction as JDBC org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@6f98ca1d]
Initiating transaction rollback
Rolling back JPA transaction on EntityManager [SessionImpl(1649890926<open>)]
Closing JPA EntityManager [SessionImpl(1649890926<open>)] after transaction
Resuming suspended transaction after completion of inner transaction
Initiating transaction rollback
Rolling back JPA transaction on EntityManager [SessionImpl(1447696365<open>)]
Closing JPA EntityManager [SessionImpl(1447696365<open>)] after transaction

Solution

  • Your understanding is wrong. You're not catching the exception thrown by smsClient.sendSms(), so it's thrown from placeOrder(), so the transaction started for this method is rollbacked.

    Besides, making sendSms() transactional doesn't make much sense, since sending an SMS most probably doesn't interact with any transactional resource.