Search code examples
javaspringspring-bootspring-data-jpawebsphere

@Transactional doesn't work with @Async in Websphere


I am trying to use @Transactional inside @Async method to track file conversion process as follows:

@Async
@Transactional
public void convertFile(String documentId) {
     
    CustomLog customLog = new CustomLog();
    customLog.setStatus("IN_PROGRESS");
    mySpringDataRepo.save(customLog);

    try {
      doConvertFunction(documentId);    
    } catch (Exception e) {
      customLog.setStatus("FAIL");
      mySpringDataRepo.save(customLog);
      return;
    }

    customLog.setStatus("SUCCESS");
    mySpringDataRepo.save(customLog);   
}

I am using the following technologies :

  • Spring Boot 2.7.18
  • Hibernate Core 5.6.15.Final
  • Websphere ND 9.0.5.13
  • Java 1.8

My custom EntityManager configuration:

@Primary
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,
    DataSource dataSource) {
    LocalContainerEntityManagerFactoryBean em = builder.dataSource(dataSource).packages("com.myapp").persistenceUnit("MyEntityManagerFactory").properties(jpaProperties()).build();
    JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    em.setJpaVendorAdapter(vendorAdapter);
    return em;
}

protected Map<String, Object> jpaProperties() {
        Map<String, Object> props = new HashMap<>();
        props.put("hibernate.physical_naming_strategy", CamelCaseToUnderscoresNamingStrategy.class.getName());
        props.put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName());
        return props;
}

When running the above code on Tomcat 9 It works perfectly fine, but when trying to run it on Websphere the transaction doesn't open and the save method doesn't get executed at all !

I made several tries with no luck as follows:

  1. Using @Transactional(propagation = Propagation.REQUIRES_NEW)

  2. Extracting the save method to a new separate service method which is as follows:

    @Service
    public class MyService {
    
      @Autowired
      private MySpringDataRepo mySpringDataRepo;
    
      @Transactional(propagation = Propagation.REQUIRES_NEW)
      public CustomLog save(CustomLog customLog) {
         mySpringDataRepo.save(customLog);
         return customLog;
      }
    
    }
    

UPDATE:

The only solution worked on Websphere was by creating EntityManager from EntityManagerFactory as follows:

@Service
public class CustomLogService {

    @Autowired
    private EntityManagerFactory entityManagerFactory;

    
    public Long save(CustomLog customLog) {
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        entityManager.getTransaction().begin();
        entityManager.persist(customLog);
        entityManager.flush();
        entityManager.getTransaction().commit();
        return customLog.getId();
    }

    public CustomLog find(Long id) {
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        entityManager.getTransaction().begin();
        CustomLog customLog = entityManager.find(CustomLog.class, id);
        entityManager.getTransaction().commit();
        return customLog;
    }

    public void update(CustomLog customLog) {
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        entityManager.getTransaction().begin();
        entityManager.merge(customLog);
        entityManager.getTransaction().commit();
    }

}

and changed my @Async code to be as follows:

@Async
public void convertFile(String documentId) {

    CustomLog customLog = new CustomLog();
    customLog.setStatus("IN_PROGRESS");
    customLogService.save(customLog);

    try {
       doConvertFunction(documentId);    
    } catch (Exception e) {
      customLog = customLogService.find(customLog.getId());
      customLog.setStatus("FAIL");
      customLogService.update(customLog);
      return;
    }

    customLog = customLogService.find(customLog.getId());
    customLog.setStatus("SUCCESS");
    customLogService.update(customLog);

}

I tried to inject entityManager as follows but it doesn't work only creating entityManager from EntityManagerFactory works:

@PersistenceContext(name = "MyEntityManagerFactory")
private EntityManager entityManager;

Are there any disadvantages/things to consider in this solution because I will be heavily using this method and it returns a new EntityManager instance each time it is invoked?


Solution

  • Update your configuration code to this :

    @Configuration
    @EnableJpaRepositories(basePackages = { "com.myapp.dao" }, transactionManagerRef = "websphereTxManager")
    @EnableTransactionManagement
    public class WebSphereDBConfiguration {
    
        @Resource(lookup = "jdbc/sample", name = "java:comp/env/jdbc/sample")
        private DataSource dataSource;
    
        @Primary
        @Bean
        public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
            LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
            entityManagerFactoryBean.setPackagesToScan("com.myapp.entity");
            entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
            entityManagerFactoryBean.setDataSource(dataSource);
            entityManagerFactoryBean.setJpaPropertyMap(jpaProperties());
            return entityManagerFactoryBean;
        }
    
        @Bean
        public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
            return entityManagerFactoryBean.getObject();
        }
    
        @Bean
        public PlatformTransactionManager websphereTxManager(EntityManagerFactory entityManagerFactory) {
            JpaTransactionManager transactionManager = new JpaTransactionManager();
            transactionManager.setEntityManagerFactory(entityManagerFactory);
            return transactionManager;
        }
    
    
        protected Map<String, Object> jpaProperties() {
            Map<String, Object> props = new HashMap<>();
            props.put("hibernate.physical_naming_strategy", CamelCaseToUnderscoresNamingStrategy.class.getName());
            props.put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName());
            return props;
        }
    
    
    }
    

    Use the same code :

    @Async
    @Transactional(transactionManager = "websphereTxManager")
    public void convertFile(String documentId) {
         
        CustomLog customLog = new CustomLog();
        customLog.setStatus("IN_PROGRESS");
        mySpringDataRepo.save(customLog);
        
        try {
          doConvertFunction(documentId);    
        } catch(Exception e) {
          customLog.setStatus("FAIL");
          mySpringDataRepo.save(customLog);
          return;
        }
    
        customLog.setStatus("SUCCESS");
        mySpringDataRepo.save(customLog);
           
    }
    

    Few points to be noted:

    1. Creating a bean of LocalContainerEntityManagerFactoryBean.
    2. Creating a bean of EntityManagerFactory via injecting LocalContainerEntityManagerFactoryBean and getting the object via LocalContainerEntityManagerFactoryBean.getObject(). Also, make that bean as the primary bean for the whole application.
    3. You will need to create a custom transaction manager. So, creating a JpaTransactionManager bean via injection and using the EntityManagerFactory bean.
    4. You can see this annotation @EnableTransactionManagement. By use of this annotation, you will tell Spring to use this configuration class to manage your transaction.
    5. See this annotation @EnableJpaRepositories. By this annotation, you will tell Spring which packages to scan containing the JPA repositories and also tell which transaction manager to use via this property transactionManagerRef by default.
    6. You have to configure datasource on the WebSphere (I guess you already did) and inject it via @Resource and set that datasource in the LocalContainerEntityManagerFactoryBean bean.

    See if this helps.