Reputation: 141
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
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