Calling @Transactional methods from another thread (Runnable)
Asked Answered
S

3

25

Is there any simple solution to save data into database using JPA in a new thread?

My Spring based web application allows user to manage scheduled tasks. On runtime he can create and start new instances of predefined tasks. I am using spring's TaskScheduler and everything works well.

But I need to save boolean result of every fired task into database. How can I do this?

EDIT: I have to generalize my question: I need to call a method on my @Service class from tasks. Because the task result has to be "processed" before saving into database.

EDIT 2: Simplified version of my problematic code comes here. When saveTaskResult() is called from scheduler, message is printed out but nothing is saved into db. But whenever I call saveTaskResult() from controller, record is correctly saved into database.

@Service
public class DemoService {

    @Autowired
    private TaskResultDao taskResultDao;

    @Autowired
    private TaskScheduler scheduler;

    public void scheduleNewTask() {
        scheduler.scheduleWithFixedDelay(new Runnable() {

            public void run() {
                // do some action here
                saveTaskResult(new TaskResult("result"));
            }

        }, 1000L);
    }

    @Transactional
    public void saveTaskResult(TaskResult result) {
        System.out.println("saving task result");
        taskResultDao.persist(result);
    }

}
Supporter answered 30/6, 2012 at 15:50 Comment(6)
What exactly is the problem? You start a thread, you call your Spring services exactly as you would have done if you had not started a thread, and everything should be OK.Confiteor
Problem is target business method is @Transactional. When I call this method in run(), data not get persisted. (I have updated question title)Redbug
The transactional interceptor doesn't care if the thread calling the method is a thread created by your servlet container, or a thread created by you. It should work. Show us some code.Confiteor
Sample code added into original questionRedbug
Which Spring transaction manager are you using?Keare
org.springframework.orm.jpa.JpaTransactionManagerRedbug
C
44

The problem with your code is that you expect a transaction to be started when you call saveTaskResult(). This won't happen because Spring uses AOP to start and stop transactions.

If you get an instance of a transactional Spring bean from the bean factory, or through dependency injection, what you get is in fact a proxy around the bean. This proxy starts a transaction before calling the actual method, and commits or rollbacks the transaction once the method has completed.

In this case, you call a local method of the bean, without going through the transactional proxy. Put the saveTaskResult() method (annotated with @Transactional) in another Spring bean. Inject this other Spring bean into DemoService, and call the other Spring bean from the DemoService, and everything will be fine.

Confiteor answered 30/6, 2012 at 21:8 Comment(1)
Thanks a million. this solution works with CompletableFuture like charm.Telegraphese
H
8

Transactions are held at thread local storage.
If your other method is running a thread with @Transactional annotation.
The default is set to REQUIRED and this means that if you run a method annotated with @Transacitonal from a different thread, you will have a new transaction (as there is no transaction held in the thread local storage of this thread).

Herdic answered 30/6, 2012 at 17:36 Comment(2)
This is actually very valid, it's not an answer though it better be a comment. In the given example it doesn't make a difference, but if the scheduleNewTask was already @Transactional then indeed the saveTaskResult will start a new transaction even if it's set to REQUIRED which would join an existing transaction if exist.Hydroplane
Here you will find a trick for this BUT, sometimes there is no sense in this. F.i. all public methods in Oracle connection are synchronized Oracle Docs This approach is going to use same connection. You won't win anything even if it works (haven't tested it by myself so can't be sure). Kinda notice, check your connection documentation.Gregarious
S
6

Another option (besides creating separate Spring Bean with @Transactional method in it) is manually seting up transaction by using TransactionTemplate.

final TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
   taskExecutor.execute(new Runnable() {
        @Override
        public void run() {
            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                   dao.update(object);
                }
            });
        }
    });  
Soliloquy answered 22/9, 2021 at 10:24 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.