Using Multiple DataSources In Spring Batch

Multiple DataSources In Spring Batch

Here I am going to show you how to use multiple datasources in spring batch application. You may need to use multiple data sources for various reasons, for example, you may need to store table metadata for spring batch into h2 database and other business data into Oracle, MySQL or any other database.

Or another situation may occur where in production environment you do not have permission to create meta data tables for spring batch then you can use h2 database for storing meta data related information.

Prerequisites

Java 19, Spring Boot 3.1.2, Maven 3.8.5, MySQL 8.0.31

Project Setup

You can create a maven based project in your favorite IDE or tool. You can use the following pom.xml file for your project.

In the following build file notice I have used two different database dependencies – H2 and MySQL. The H2 in-memory database will be used for storing spring batch meta data tables and MySQL database will be used for storing actual business data.

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.roytuts</groupId>
	<artifactId>spring-batch-multiple-datasources</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>19</maven.compiler.source>
		<maven.compiler.target>19</maven.compiler.target>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.2</version>
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-batch</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.31</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

MySQL Database Config

I am using only MySQL database configurations in src/main/resources/application.properties file and you don’t need to configure the H2 database as it is in-memory database, so it will be available as long as the application is live.

spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/roytuts
spring.datasource.username=root
spring.datasource.password=root

spring.main.allow-bean-definition-overriding=true

The spring.main.allow-bean-definition-overriding=true override the implementations of spring beans when you want to override the definition or implementation of any spring bean.

MySQL Table

The following MySQL table stores the person details from CSV file which is used in the spring batch example.

CREATE DATABASE IF NOT EXISTS `roytuts`;
USE `roytuts`;

CREATE TABLE IF NOT EXISTS `persons` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `email` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

DataSource Config

The following configuration class defines the data sources for H2 database and MySQL database. It also defines the transaction managers for both databases. Whenever you are working with multiple data sources or transaction managers you need to make one of them as primary.

@Configuration
public class DataSourceConfig {

	@Autowired
	private Environment environment;

	@Primary
	@Bean(name = "h2DataSource")
	public DataSource h2DataSource() {
		EmbeddedDatabaseBuilder embeddedDatabaseBuilder = new EmbeddedDatabaseBuilder();
		return embeddedDatabaseBuilder.addScript("classpath:org/springframework/batch/core/schema-drop-h2.sql")
				.addScript("classpath:org/springframework/batch/core/schema-h2.sql").setType(EmbeddedDatabaseType.H2)
				.build();
	}

	@Bean(name = "mySQLDataSource")
	public DataSource mySQLDataSource() {
		return DataSourceBuilder.create().driverClassName(environment.getProperty("spring.datasource.driverClassName"))
				.url(environment.getProperty("spring.datasource.url"))
				.username(environment.getProperty("spring.datasource.username"))
				.password(environment.getProperty("spring.datasource.password")).build();
	}

	@Bean
	public PlatformTransactionManager mySQLDataSourceTransactionManager() {
		return new DataSourceTransactionManager(mySQLDataSource());
	}

	@Bean
	public NamedParameterJdbcTemplate namedParameterJdbcTemplate() {
		return new NamedParameterJdbcTemplate(mySQLDataSource());
	}

	@Primary
	@Bean(name = "platformTransactionManager")
	public PlatformTransactionManager platformTransactionManager() {
		return new DataSourceTransactionManager(h2DataSource());
	}

}

FieldSet Mapper

The field set mapper is used to set the value to the appropriate object after reading from the input source.

public class UserFieldSetMapper implements FieldSetMapper<User> {

	@Override
	public User mapFieldSet(FieldSet fieldSet) throws BindException {
		User user = new User();
		user.setName(fieldSet.readString(0));
		user.setEmail(fieldSet.readString(1));
		return user;
	}

}

Item Processor

Item processor basically processes each item to be transformed into something else before being written to the destination.

public class UserItemProcessor implements ItemProcessor<User, User> {

	@Override
	public User process(final User user) throws Exception {
		final String domain = "roytuts.com";
		
		final String name = user.getName().toUpperCase();
		final String email = user.getName() + "@" + domain;
		final User transformedUser = new User(name, email);
		
		System.out.println("Converting [" + user + "] => [" + transformedUser + "]");
		
		return transformedUser;
	}

}

Prepared Statement

This is required to write the value of each item to the destination and here I am going to write each item to the database table.

public class PersonsPreparedStatementSetter implements ItemPreparedStatementSetter<User> {

	@Override
	public void setValues(User item, PreparedStatement ps) throws SQLException {
		ps.setString(1, item.getName());
		ps.setString(2, item.getEmail());
	}

}

Spring Batch Config

Spring batch configuration configures JobRepository, Job, ItemReader, ItemProcessor, ItemWriter for the spring batch processing mechanism.

You don’t need to use @EnableBatchProcessing in spring batch 5 version. The @Configuration annotation is enough for configuring the batch things.

@Configuration
public class SpringBatchConfig {

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

	@Autowired
	private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

	@Autowired
	private PlatformTransactionManager platformTransactionManager;

	private static final String QUERY_INSERT_PERSONS = "INSERT " + "INTO persons(name, email) " + "VALUES (?, ?)";

	@Bean
	public JobRepository jobRepository() throws Exception {
		JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
		factory.setDataSource(dataSource);
		factory.setTransactionManager(platformTransactionManager);
		factory.afterPropertiesSet();
		return factory.getObject();
	}

	@Bean
	// creates an item reader
	public ItemReader<User> reader() {
		FlatFileItemReader<User> reader = new FlatFileItemReader<User>();
		// look for file user.csv
		reader.setResource(new ClassPathResource("person.csv"));
		// line mapper
		DefaultLineMapper<User> lineMapper = new DefaultLineMapper<User>();
		// each line with comma separated
		lineMapper.setLineTokenizer(new DelimitedLineTokenizer());
		// map file's field with object
		lineMapper.setFieldSetMapper(new UserFieldSetMapper());
		reader.setLineMapper(lineMapper);
		return reader;
	}

	@Bean
	// creates an instance of our UserItemProcessor for transformation
	public ItemProcessor<User, User> processor() {
		return new UserItemProcessor();
	}

	@Bean
	@Transactional(rollbackFor = Exception.class)
	// creates item writer
	public ItemWriter<User> writer() {

		JdbcBatchItemWriter<User> batchItemWriter = new JdbcBatchItemWriter<>();
		batchItemWriter.setJdbcTemplate(namedParameterJdbcTemplate);
		batchItemWriter.setSql(QUERY_INSERT_PERSONS);

		ItemPreparedStatementSetter<User> valueSetter = new PersonsPreparedStatementSetter();

		batchItemWriter.setItemPreparedStatementSetter(valueSetter);

		return batchItemWriter;
	}

	@Bean
	public Job importUserJob(Step step) throws Exception {
		// need incrementer to maintain execution state
		return new JobBuilder("importUserJob", jobRepository()).incrementer(new RunIdIncrementer()).flow(step).end()
				.build();
	}

	@Bean
	public Step step1(ItemReader<User> reader, ItemWriter<User> writer, ItemProcessor<User, User> processor)
			throws Exception {
		// chunk uses how much data to write at a time
		// In this case, it writes up to five records at a time.
		// Next, we configure the reader, processor, and writer
		return new StepBuilder("step1", jobRepository()).<User, User>chunk(5, platformTransactionManager).reader(reader)
				.processor(processor).writer(writer).build();
	}

}

Input File

The input file (person.csv) is a CSV (Comma Separated Value) file which has simply first name and last name pairs. This file is kept under class path folder src/main/resources.

soumitra,roy
souvik,sanyal
arup,chatterjee
suman,mukherjee
debina,guha
liton,sarkar
debabrata,poddar

VO Class

The Value Object class is a simple class which has two attributes for first and last names.

public class User {

	private String name;
	private String email;

	public User() {
	}

	public User(String name, String email) {
		this.name = name;
		this.email = email;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	@Override
	public String toString() {
		return "name: " + name + ", email:" + email;
	}

}

Spring Boot Main Class

A class is having main method and @SpringBootApplication annotation is enough to start the spring boot application.

@SpringBootApplication
public class SpringBatch {

	public static void main(String[] args) {
		SpringApplication.run(SpringBatch.class, args);
	}

}

Testing Multiple Data Sources In Spring Batch

Here is the output of the spring batch application when run by executing the main class.

Converting [name: soumitra, email:roy] => [name: SOUMITRA, email:soumitra@roytuts.com]
Converting [name: souvik, email:sanyal] => [name: SOUVIK, email:souvik@roytuts.com]
Converting [name: arup, email:chatterjee] => [name: ARUP, email:arup@roytuts.com]
Converting [name: suman, email:mukherjee] => [name: SUMAN, email:suman@roytuts.com]
Converting [name: debina, email:guha] => [name: DEBINA, email:debina@roytuts.com]
Converting [name: liton, email:sarkar] => [name: LITON, email:liton@roytuts.com]
Converting [name: debabrata, email:poddar] => [name: DEBABRATA, email:debabrata@roytuts.com]

Data in the MySQL database table gets inserted as:

spring batch multiple data sources

And the insert statements when exported be like the following:

INSERT INTO `persons` (`id`, `name`, `email`) VALUES
	(1, 'SOUMITRA', 'soumitra@roytuts.com'),
	(2, 'SOUVIK', 'souvik@roytuts.com'),
	(3, 'ARUP', 'arup@roytuts.com'),
	(4, 'SUMAN', 'suman@roytuts.com'),
	(5, 'DEBINA', 'debina@roytuts.com'),
	(6, 'LITON', 'liton@roytuts.com'),
	(7, 'DEBABRATA', 'debabrata@roytuts.com');

Hope you got an idea how to use multiple datasources in spring batch application.

Source Code

Download

Leave a Reply

Your email address will not be published. Required fields are marked *