Search code examples
springtimeouttransactional

Method must be called outside of transactional context - Spring @Transactional


I hope you're well.

I would like to find the best way to ensure that a service method is called outside of a transaction. It would be as follows:

Lets say that we have a method in the form of:

@Transactional
public void insertEntity(Entity entity){
    persistence.save(entity);
}

Now, lets say that we are invoking this method, but we need to be sure that is not called inside code that is transactional already. Following would be wrong:

@Transactional
public void enclosingTransaction() {
    //Perform long process transaction
    service.insertEntity(entity);
}

What is the best option to make our method "insertEntity" aware that is being called inside a running transaction and throw error?

Thanks!


Solution

  • You could invoke TransactionAspectSupport.currentTransactionStatus().isNewTransaction() method in order to know if the current transaction is new (i.e. it was not propagated from another @Transactional method) or not:

    @Transactional
    public void insertEntity(Entity entity){
        if (!TransactionAspectSupport.currentTransactionStatus().isNewTransaction()) {
            throw new IllegalStateException("Transaction is not new!");
        }
        persistence.save(entity);
    }
    

    The static method TransactionAspectSupport.currentTransactionStatus() returns a TransactionStatus object which represents the transaction status of the current method invocation.


    I wrote a minimal Spring MVC webapp to test your scenario (I'm omitting configuration classes and files, as well as import and packages declarations):

    TestController.java

    @RestController
    public class TestController {
    
        private static final Logger log = LoggerFactory.getLogger(TestController.class);
    
        @Autowired
        private ServiceOne serviceOne;
    
        @Autowired
        private ServiceTwo serviceTwo;
    
        @GetMapping(path = "/test-transactions")
        public String testTransactions() {
            log.info("*** TestController.testTransactions() ***");
            log.info("* Invoking serviceOne.methodOne()...");
            try {
                serviceOne.methodOne();
            }
            catch (IllegalStateException e) {
                log.error("* {} invoking serviceOne.methodOne()!", e.getClass().getSimpleName());
            }
            log.info("* Invoking serviceTwo.methodTwo()...");
            try {
                serviceTwo.methodTwo();
            }
            catch (IllegalStateException e) {
                log.error("* {} invoking serviceTwo.methodTwo()!", e.getClass().getSimpleName());
            }
            return "OK";
        }
    }
    

    ServiceOneImpl.java

    @Service
    public class ServiceOneImpl implements ServiceOne {
    
        private static final Logger log = LoggerFactory.getLogger(ServiceOneImpl.class);
    
        @Autowired
        private ServiceTwo serviceTwo;
    
        @PersistenceContext
        private EntityManager em;
    
        @Override
        @Transactional(propagation = Propagation.REQUIRED)
        public void methodOne() {
            log.info("*** ServiceOne.methodOne() ***");
            log.info("getCurrentTransactionName={}", TransactionSynchronizationManager.getCurrentTransactionName());
            log.info("isNewTransaction={}", TransactionAspectSupport.currentTransactionStatus().isNewTransaction());
            log.info("Query result={}", em.createNativeQuery("SELECT 1").getResultList());
            log.info("getCurrentTransactionName={}", TransactionSynchronizationManager.getCurrentTransactionName());
            log.info("isNewTransaction={}", TransactionAspectSupport.currentTransactionStatus().isNewTransaction());
            serviceTwo.methodTwo();
        }
    }
    

    ServiceTwoImpl.java

    @Service
    public class ServiceTwoImpl implements ServiceTwo {
    
        private static final Logger log = LoggerFactory.getLogger(ServiceTwoImpl.class);
    
        @PersistenceContext
        private EntityManager em;
    
        @Override
        @Transactional(propagation = Propagation.REQUIRED)
        public void methodTwo() {
            log.info("*** ServiceTwo.methodTwo() ***");
            log.info("getCurrentTransactionName={}", TransactionSynchronizationManager.getCurrentTransactionName());
            log.info("isNewTransaction={}", TransactionAspectSupport.currentTransactionStatus().isNewTransaction());
            if (!TransactionAspectSupport.currentTransactionStatus().isNewTransaction()) {
                log.warn("Throwing exception because transaction is not new...");
                throw new IllegalStateException("Transaction is not new!");
            }
            log.info("Query result={}", em.createNativeQuery("SELECT 2").getResultList());
            log.info("getCurrentTransactionName={}", TransactionSynchronizationManager.getCurrentTransactionName());
            log.info("isNewTransaction={}", TransactionAspectSupport.currentTransactionStatus().isNewTransaction());
        }
    }
    

    And here it is the log of the execution:

    INFO test.transactions.web.TestController - *** TestController.testTransactions() ***
    INFO test.transactions.web.TestController - * Invoking serviceOne.methodOne()...
    INFO test.transactions.service.ServiceOneImpl - *** ServiceOne.methodOne() ***
    INFO test.transactions.service.ServiceOneImpl - getCurrentTransactionName=test.transactions.service.ServiceOneImpl.methodOne
    INFO test.transactions.service.ServiceOneImpl - isNewTransaction=true
    INFO test.transactions.service.ServiceOneImpl - Query result=[1]
    INFO test.transactions.service.ServiceOneImpl - getCurrentTransactionName=test.transactions.service.ServiceOneImpl.methodOne
    INFO test.transactions.service.ServiceOneImpl - isNewTransaction=true
    INFO test.transactions.service.ServiceTwoImpl - *** ServiceTwo.methodTwo() ***
    INFO test.transactions.service.ServiceTwoImpl - getCurrentTransactionName=test.transactions.service.ServiceOneImpl.methodOne
    INFO test.transactions.service.ServiceTwoImpl - isNewTransaction=false
    WARN test.transactions.service.ServiceTwoImpl - Throwing exception because transaction is not new...
    ERROR test.transactions.web.TestController - * IllegalStateException invoking serviceOne.methodOne()!
    INFO test.transactions.web.TestController - * Invoking serviceTwo.methodTwo()...
    INFO test.transactions.service.ServiceTwoImpl - *** ServiceTwo.methodTwo() ***
    INFO test.transactions.service.ServiceTwoImpl - getCurrentTransactionName=test.transactions.service.ServiceTwoImpl.methodTwo
    INFO test.transactions.service.ServiceTwoImpl - isNewTransaction=true
    INFO test.transactions.service.ServiceTwoImpl - Query result=[2]
    INFO test.transactions.service.ServiceTwoImpl - getCurrentTransactionName=test.transactions.service.ServiceTwoImpl.methodTwo
    INFO test.transactions.service.ServiceTwoImpl - isNewTransaction=true