Search code examples
spring-boothibernatespring-data-jpaspring-async

Async Transactional Saves do not work in Spring after upgrade


We upgraded Spring Boot from 1.5.8 to 2.6.2. It has introduced a problem that has us perplexed: Transactional saves are not processing from inside spawned threads.

We use JPA managed entities on a Mysql database and make calls down to the CrudRepository to save them.

Transactions inside the main thread work fine. However, when called from an asynchronous operation things go awry:

  • both async and sync calls go through the Spring SimpleJpaRepository.save() method. But the entityManager returns the object to persist with a null id in the case of the async operation.
  • I followed the flow through in both types of calls and can see that the save propagates down to the org.hibernate.internal.SessionImpl service.
  • From there it makes its way to AbstractSaveEventListener.class and that is where the discrepancy appears to be. In the performSaveOrReplicate method (hibernate-core:5.6.3), inTrx boolean is false in the async workflow whereas it is true in the synch one. Because of that the shouldDelayIdentityInserts flag gets set and an id does not appear to be generated for any entities in this thread.

We have tried different things to get this to work. For example, we used the transactionTemplate to have some specific control here, but that has not changed the behavior.

We were originally creating this async process by using the ApplicationEventPublisher to create an event. We also tried using completablefuture and other constructs with the same result as well as annotating the method with @Async and calling it directly.


Solution

  • The issue was that, with the upgrade to Spring Boot 2.6, Spring Batch implements a new Transaction Manager.

    What we didn't realize is that this transaction manager was being autowired into our other services and did not work in this threaded context. You do not want to share a Batch processing Tx Manager with your API/misc services. Declaring a specific Transaction Manager there to keep them separate solved the issue.

    Here is an example marking a PlatformTransactionManager with the Primary annotation to test its usage explicitly.

    @Primary
    @Bean
    public PlatformTransactionManager platformTransactionManager(DataSource dataSource) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory(dataSource).getObject());
        return transactionManager;
    }