Calling @Transactional methods from another thread (Runnable)

Ondřej Míchal picture Ondřej Míchal · Jun 30, 2012 · Viewed 14.5k times · Source

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);
    }

}

Answer

JB Nizet picture JB Nizet · Jun 30, 2012

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.