shadows
shadows

Reputation: 141

TransactionRequiredException with Spring Batch

I have a Spring Boot app and I wanted to add a service with Spring Batch. After the implementation of Spring Batch, the application throws this exception for the services that does the update/delete operations:

org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query

However these services was working fine without the Spring Batch implementation. Also in these services' service layer there is already Spring's @Transactional annotation.

I'm able to overcome this exception with this implementation:

@Modifying
@Override
public void execute(){
    Session session = (Session) entityManager.getDelegate();
    Transaction transaction = session.beginTransaction();
    Query nativeQuery = session.createNativeQuery("execute procedure ...");
    nativeQuery.executeUpdate();
    transaction.commit();
}

However, normally, I don't need to do beginTransaction and commit transaction, since there is Spring's @Transactional annotation.

This issue appeared after the Spring Batch implemenation. Here is my Spring Batch implementation, just like hello world:

@Configuration
@EnableBatchProcessing
public class BatchConfig {
    @Autowired
    public JobBuilderFactory jobBuilderFactory;
    
    @Autowired
    public StepBuilderFactory stepBuilderFactory;
    
    @Bean
    public Job job1() {
        return jobBuilderFactory.get("job1")
                .start(step1())
                .build();
    }
    
    private final String[] messages = {"m1", "m2", "m3"};
    private int count = 0;
    
    @Bean
    public Step step1() {
        return stepBuilderFactory.get("step1")
                .chunk(1)
                .reader((ItemReader<String>) () -> {
                    return count >= messages.length ? null : messages[count++];
                })
                .processor((ItemProcessor<Object, Object>) o -> {
                    return o + " from write";
                })
                .writer(System.out::println)
                .build();
    }
}

Controller:

@RestController
@RequestMapping
public class ProcessController {
    @Autowired
    JobLauncher jobLauncher;
    @Autowired
    Job job1;
    
    @PostMapping("/process")
    public ResponseEntity<?> process() throws Exception {
        jobLauncher.run(job1, new JobParameters());
        return ResponseEntity.accepted().build();
    }
}

Since the database that I'm using is not supported by Spring Batch, I needed to change the database type as suggested in Spring Batch doc:

@Component
public class CustomBatchConfigurer extends DefaultBatchConfigurer {
    private final DataSource dataSource;
    
    public CustomBatchConfigurer(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    @Override
    protected JobRepository createJobRepository() throws Exception {
        JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
        factory.setDataSource(dataSource);
        factory.setDatabaseType("h2");
        factory.setTransactionManager(getTransactionManager());
        factory.setIncrementerFactory(new CustomDefaultDataFieldMaxValueIncrementerFactory(dataSource));
        factory.afterPropertiesSet();
        return factory.getObject();
    }
}

Properties:

spring.batch.job.enabled=false
spring.batch.jdbc.initialize-schema=never

I tested Spring Batch implementation and It works fine, but somehow it brokes the transaction manager I guess.

Do you have any idea about this issue?

Upvotes: 1

Views: 922

Answers (1)

httPants
httPants

Reputation: 2123

Assuming your spring batch tables are in the same database schema as your other database tables, you can use the same transaction manager for your repository and spring batch.

By default Spring Batch creates a DataSourceTransactionManager. You should use a JpaTransactionManager instead, so the spring data repository knows that a transaction is active and can join that transaction.

As an example, your repository configuration should look something like this...

@Configuration
@EnableJpaRepositories(entityManagerFactoryRef = "myEntityManagerFactory",
    transactionManagerRef = "myTransactionManager", basePackageClasses = {
            aaa.bbb.ccc.MyRepository.class })
public class MyRepositoryConfiguration {

    @Autowired
    @Qualifier("dataSource")
    private DataSource dataSource;

    @Bean
    public PlatformTransactionManager myTransactionManager() throws NamingException {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setDataSource(dataSource;);
        tm.setEntityManagerFactory(myEntityManagerFactory().getObject());
        return tm;
    }

    @Bean(name = "myEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean myEntityManagerFactory() throws NamingException {
        LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
        emfBean.setDataSource(dataSource);
        emfBean.setPackagesToScan("aaa.bbb.ccc");
        emfBean.setBeanName("myEntityManagerFactory");
        return emfBean;
    }
}

Then when configuring Spring Batch, you can specify myTransactionManager as the transaction manager...

@Component
@EnableBatchProcessing
public class BatchConfiguration implements BatchConfigurer {

    private JobRepository jobRepository;
    private JobExplorer jobExplorer;
    private JobLauncher jobLauncher;

    @Autowired
    @Qualifier(value = "myTransactionManager")
    private PlatformTransactionManager myTransactionManager;

    @Autowired
    @Qualifier(value = "dataSource")
    private DataSource batchDataSource;

    @Override
    public JobRepository getJobRepository() {
        return this.jobRepository;
    }

    @Override
    public JobLauncher getJobLauncher() {
        return this.jobLauncher;
    }

    @Override
    public JobExplorer getJobExplorer() {
        return this.jobExplorer;
    }

    @Override
    public PlatformTransactionManager getTransactionManager() {
        return this.myTransactionManager;
    }

    protected JobLauncher createJobLauncher() throws Exception {
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository);
        jobLauncher.afterPropertiesSet();
        return jobLauncher;
    }

    protected JobRepository createJobRepository() throws Exception {
        JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
        factory.setDatabaseType(String.valueOf(DatabaseType.ORACLE));
        factory.setDataSource(this.batchDataSource);
        factory.setTransactionManager(getTransactionManager());
        factory.setIsolationLevelForCreate("ISOLATION_READ_COMMITTED");
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @PostConstruct
    public void afterPropertiesSet() throws Exception {
        this.jobRepository = createJobRepository();
        JobExplorerFactoryBean jobExplorerFactoryBean = new JobExplorerFactoryBean();
        jobExplorerFactoryBean.setDataSource(this.batchDataSource);
        jobExplorerFactoryBean.afterPropertiesSet();
        this.jobExplorer = jobExplorerFactoryBean.getObject();
        this.jobLauncher = createJobLauncher();
    }
}

Upvotes: 2

Related Questions