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!
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 package
s 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