How to Configure Multiple Data Sources, Entity Managers, Transaction Managers in Spring Boot

Earlier, we have explored various approaches for Joining Unrelated Entities and Mapping the Result to POJO with Spring Data JPA and Hibernate. In this article, we are gonna configure multiple databases, entity managers, transaction managers, and Hikari connection pool in a Spring Boot Application.

Project Structure

This is how our project structure will look like. The model and repository packages are database-specific. So that we can specify the respective package in the database-specific configuration classes for scanning entities and repositories. The entities and repositories of multiple databases should not be put together in the same package.

Spring Boot Multiple Data Sources Project Structure

Dependencies

pom.xml

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.javachinna</groupId>
	<artifactId>multiple-datasources</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>multiple-datasources</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

Configure Data Sources and Connection Pool

Let’s assume that we have 2 different databases namely PRIME and PRODUCT. PRIME is the primary database that is used for user management and PRODUCT is used for product management. Now let’s configure both the databases in our Spring Boot application. Also, we are gonna configure Hikari CP as that is the default connection pool used by Spring Boot 2.x

Configure Application Properties

application.properties

### Prime Database Details
app.datasource.prime.url=jdbc:mysql://localhost:3306/prime?createDatabaseIfNotExist=true
app.datasource.prime.username=root
app.datasource.prime.password=secret
app.datasource.prime.driver-class-name=com.mysql.cj.jdbc.Driver
### Prime Database Connection Pool Details
app.datasource.prime.hikari.idle-timeout=10000
app.datasource.prime.hikari.maximum-pool-size=10
app.datasource.prime.hikari.minimum-idle=5
app.datasource.prime.hikari.pool-name=PrimeHikariPool

### Product Database Details
app.datasource.product.url=jdbc:mysql://localhost:3306/product?createDatabaseIfNotExist=true
app.datasource.product.username=root
app.datasource.product.password=secret
app.datasource.product.driver-class-name=com.mysql.cj.jdbc.Driver
### Product Database Connection Pool Details
app.datasource.product.hikari.idle-timeout=10000
app.datasource.product.hikari.maximum-pool-size=10
app.datasource.product.hikari.minimum-idle=5
app.datasource.product.hikari.pool-name=ProductHikariPool

# Hibernate props
hibernate.hbm2ddl.auto=create
hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

Configure Primary Database

PrimeDataSourceConfiguration.java

@Primary annotation indicates that a bean should be given preference when multiple candidates are qualified to autowire a single-valued dependency. If exactly one ‘primary’ bean exists among the candidates, it will be the autowired value. In simple words, it is used to mark a bean as a default one. This is helpful when there are multiple beans of the same type defined and we wanna inject a bean without specifying its name.

package com.javachinna.config;

import java.util.HashMap;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import com.zaxxer.hikari.HikariDataSource;

@Configuration
@EnableJpaRepositories(basePackages = "com.javachinna.repo.prime", entityManagerFactoryRef = "primeEntityManager", transactionManagerRef = "primeTransactionManager")
public class PrimeDataSourceConfiguration {

	@Autowired
	Environment env;

	@Bean
	@Primary
	@ConfigurationProperties(prefix = "app.datasource.prime")
	public DataSourceProperties primeDataSourceProperties() {
		return new DataSourceProperties();
	}

	@Bean
	@Primary
	public DataSource primeDataSource() {
		return primeDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
	}

	@Bean
	@Primary
	public LocalContainerEntityManagerFactoryBean primeEntityManager() {
		LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
		em.setDataSource(primeDataSource());
		em.setPackagesToScan("com.javachinna.model.prime");
		HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
		em.setJpaVendorAdapter(vendorAdapter);
		HashMap<String, Object> properties = new HashMap<>();
		properties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
		properties.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
		em.setJpaPropertyMap(properties);
		return em;
	}

	@Bean
	@Primary
	public PlatformTransactionManager primeTransactionManager() {
		JpaTransactionManager transactionManager = new JpaTransactionManager();
		transactionManager.setEntityManagerFactory(primeEntityManager().getObject());
		return transactionManager;
	}
}

Notice that we have specified com.javachinna.repo.prime and com.javachinna.model.prime as the package names for scanning JPA repositories and entities of the PRIME database respectively.

Configure Secondary Database

ProductDataSourceConfiguration.java

package com.javachinna.config;

import java.util.HashMap;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import com.zaxxer.hikari.HikariDataSource;

@Configuration
@EnableJpaRepositories(basePackages = "com.javachinna.repo.product", entityManagerFactoryRef = "productEntityManager", transactionManagerRef = "productTransactionManager")
public class ProductDataSourceConfiguration {

	@Autowired
	Environment env;

	@Bean
	@ConfigurationProperties(prefix = "app.datasource.product")
	public DataSourceProperties productDataSourceProperties() {
		return new DataSourceProperties();
	}

	@Bean
	public DataSource productDataSource() {
		return productDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
	}

	@Bean
	public LocalContainerEntityManagerFactoryBean productEntityManager() {
		LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
		em.setDataSource(productDataSource());
		em.setPackagesToScan("com.javachinna.model.product");
		HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
		em.setJpaVendorAdapter(vendorAdapter);
		HashMap<String, Object> properties = new HashMap<>();
		properties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
		properties.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
		em.setJpaPropertyMap(properties);
		return em;
	}

	@Bean
	public PlatformTransactionManager productTransactionManager() {
		JpaTransactionManager transactionManager = new JpaTransactionManager();
		transactionManager.setEntityManagerFactory(productEntityManager().getObject());
		return transactionManager;
	}
}

Notice that we have specified com.javachinna.repo.product and com.javachinna.model.product as the package names for scanning JPA repositories and entities of the PRODUCT database respectively.

Create JPA Entities and POJO’s

UserInfo.java

This POJO class is used for maping the user info from the native query resultset in prime database.

package com.javachinna.model.prime;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
	private String id;
	private String name;
}

Role.java

This entity maps to the role table in the prime database

package com.javachinna.model.prime;

import java.io.Serializable;
import java.util.Objects;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * The persistent class for the role database table.
 * 
 */
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Role implements Serializable {
	private static final long serialVersionUID = 1L;
	public static final String USER = "USER";
	public static final String ADMIN = "ADMIN";
	public static final String ROLE_USER = "ROLE_USER";
	public static final String ROLE_ADMIN = "ROLE_ADMIN";

	@Id
	@Column(name = "ROLE_ID")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int roleId;

	private String name;

	// bi-directional many-to-many association to User
	@ManyToMany(mappedBy = "roles")
	private Set<User> users;

	public Role(String name) {
		this.name = name;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(final Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		return Objects.equals(name, ((Role) obj).getName());
	}

	@Override
	public String toString() {
		final StringBuilder builder = new StringBuilder();
		builder.append("Role [name=").append(name).append("]").append("[id=").append(roleId).append("]");
		return builder.toString();
	}
}

User.java

This entity maps to the user table in the prime database

package com.javachinna.model.prime;

import java.util.Set;

import javax.persistence.Column;
import javax.persistence.ColumnResult;
import javax.persistence.ConstructorResult;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.SqlResultSetMapping;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Entity
@NoArgsConstructor
@SqlResultSetMapping(name = "UserInfoMapping", classes = @ConstructorResult(targetClass = UserInfo.class, columns = {@ColumnResult(name = "user_id", type = String.class),
		@ColumnResult(name = "username", type = String.class)}))
public class User {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "USER_ID")
	private Long id;
	private String username;
	private String password;
	// bi-directional many-to-many association to Role
	@ManyToMany
	@JoinTable(name = "user_role", joinColumns = {@JoinColumn(name = "USER_ID")}, inverseJoinColumns = {@JoinColumn(name = "ROLE_ID")})
	private Set<Role> roles;

	/**
	 * @param username
	 * @param password
	 */
	public User(String username, String password, Set<Role> roles) {
		this.username = username;
		this.password = password;
		this.roles = roles;
	}
}

ProductInfo.java

This POJO class is used for maping the product info from the native SQL query resultset in product database.

package com.javachinna.model.product;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProductInfo {
	private String id;
	private String name;
	private String price;
}

Product.java

This entity maps to the product table in the product database

package com.javachinna.model.product;

import javax.persistence.ColumnResult;
import javax.persistence.ConstructorResult;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SqlResultSetMapping;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Entity
@NoArgsConstructor
@SqlResultSetMapping(name = "ProductInfoMapping", classes = @ConstructorResult(targetClass = ProductInfo.class, columns = {@ColumnResult(name = "id", type = String.class),
		@ColumnResult(name = "name", type = String.class), @ColumnResult(name = "price", type = String.class)}))
public class Product {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String name;
	private String price;
	/**
	 * @param name
	 * @param price
	 */
	public Product(String name, String price) {
		this.name = name;
		this.price = price;
	}

}

Create Spring Data Repository and DAO Classes

We are gonna create User and Role repositories for the User and Role tables respectively in order to demonstrate CRUD operations on more than one table from one database.

RoleRepository.java

package com.javachinna.repo.prime;

import org.springframework.data.jpa.repository.JpaRepository;

import com.javachinna.model.prime.Role;

public interface RoleRepository extends JpaRepository<Role, Long> {
	Role findByName(String name);
}

UserRepository.java

package com.javachinna.repo.prime;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.javachinna.model.prime.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

	User findByUsername(String userName);
}

UserInfoRepository.java

This is just a DAO class to demonstrate how the primary entity manager can be injected without specifying the name primeEntityManager using the @Qualifier annotation.

package com.javachinna.repo.prime;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.hibernate.procedure.ProcedureOutputs;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.javachinna.model.prime.UserInfo;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Repository
@Transactional
public class UserInfoRepository {

	@PersistenceContext
	private EntityManager entityManager;

	@SuppressWarnings("unchecked")
	public List<UserInfo> getUerInfo() {
		List<UserInfo> list = new ArrayList<>();
		Query query = entityManager.createNativeQuery("select user_id, username from user", "UserInfoMapping");
		try {
			// Execute query
			list = query.getResultList();
		} catch (Exception e) {
			log.error("Error while querying the db", e);
		} finally {
			try {
				query.unwrap(ProcedureOutputs.class).release();
			} catch (Exception e) {
			}
		}
		return list;
	}
}

Notice that we have annotated this class using @Transactional without specifying the transaction manager name. This means the primary transaction manager will be used by default.

ProductRepository.java

This is a JPA respository class used for Product CRUD operations in the secondary database.

@Transactional indicates that a transaction is required to execute the update query. Here we don’t need to specify the transaction manager explicitly since we have already specified productTransactionManager using @EnableJpaRepositories annotation in the ProductDataSourceConfiguration class.

@Modifying annotation indicates a query method should be considered as modifying query as that changes the way it needs to be executed. This annotation is only considered if used on query methods defined through a @Query annotation.

package com.javachinna.repo.product;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.javachinna.model.product.Product;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

	@Transactional
	@Modifying
	@Query("update Product set name=:name where id=:id")
	void updateProduct(String name, Long id);
}

ProductInfoRepository.java

This is just a DAO class to demonstrate how the secondary entity manager can be auto wired with @Qualifier annotation and used for querying the secondary database.

package com.javachinna.repo.product;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.hibernate.procedure.ProcedureOutputs;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.javachinna.model.product.ProductInfo;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Repository
public class ProductInfoRepository {

	@Autowired
	@Qualifier("productEntityManager")
	private EntityManager entityManager;

	@SuppressWarnings("unchecked")
	public List<ProductInfo> getProductInfo() {
		List<ProductInfo> list = new ArrayList<>();
		Query query = entityManager.createNativeQuery("select id, name, price from product", "ProductInfoMapping");
		try {
			// Execute query
			list = query.getResultList();
		} catch (Exception e) {
			log.error("Error while querying the db", e);
		} finally {
			try {
				query.unwrap(ProcedureOutputs.class).release();
			} catch (Exception e) {
			}
		}
		return list;
	}

	@Transactional("productTransactionManager")
	public void updateProductInfo(String name, Long id) {
		Query query = entityManager.createQuery("update Product set name=:name where id=:id");
		try {
			// Execute query
			query.setParameter("name", name);
			query.setParameter("id", id);
			query.executeUpdate();
		} catch (Exception e) {
			log.error("Error while querying the db", e);
		} finally {
			try {
				query.unwrap(ProcedureOutputs.class).release();
			} catch (Exception e) {
			}
		}
	};
}

Notice that we have annotated the updateProductInfo() method with @Transactional("productTransactionManager") annotation which means productTransactionManager will be used here.

Create Spring Boot Main Application

MultipleDatasourcesApplication.java

package com.javachinna.multipledatasources;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.javachinna")
public class MultipleDatasourcesApplication {

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

Create JUnit Tests

PrimeDataSourceTests.java

@TestInstance(Lifecycle.PER_CLASS) annotation is used to set the test instance lifecycle mode to PER_CLASS which means the test instance state is shared between test methods in a given test class as well as between non-static @BeforeAll and @AfterAll methods in the test class. If we don’t use this annotation, then we can’t use @BeforeAll annotation on a non-static method. If we make the init method static, then we can’t use the autowired repository in it.

package com.javachinna.multipledatasources;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.Set;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.javachinna.model.prime.Role;
import com.javachinna.model.prime.User;
import com.javachinna.model.prime.UserInfo;
import com.javachinna.repo.prime.RoleRepository;
import com.javachinna.repo.prime.UserInfoRepository;
import com.javachinna.repo.prime.UserRepository;

@SpringBootTest
@TestInstance(Lifecycle.PER_CLASS)
class PrimeDataSourceTests {

	@Autowired
	private RoleRepository roleRepository;

	@Autowired
	private UserRepository userRepository;

	@Autowired
	private UserInfoRepository userInfoRepository;

	@BeforeAll
	public void init() {
		Role userRole = roleRepository.save(new Role(Role.USER));
		userRepository.save(new User("test", "secret", Set.of(userRole)));
	}

	@Test
	public void getUserTest() {
		List<User> list = userRepository.findAll();
		assertThat(list).isNotEmpty();
	}

	@Test
	public void getUserInfoTest() {
		List<UserInfo> list = userInfoRepository.getUerInfo();
		assertThat(list).isNotEmpty();
		assertThat(list.get(0).getName()).isEqualTo("test");
	}

}

ProductDataSourceTests.java

package com.javachinna.multipledatasources;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.javachinna.model.product.Product;
import com.javachinna.model.product.ProductInfo;
import com.javachinna.repo.product.ProductInfoRepository;
import com.javachinna.repo.product.ProductRepository;

@SpringBootTest
@TestInstance(Lifecycle.PER_CLASS)
class ProductDataSourceTests {

	@Autowired
	private ProductRepository productRepository;

	@Autowired
	private ProductInfoRepository productInfoRepository;

	@BeforeAll
	public void init() {
		Product product = new Product("phone", "secret");
		product = productRepository.save(product);
	}

	@Test
	public void getProductTest() {
		List<Product> list = productRepository.findAll();
		assertThat(list).isNotEmpty();
	}

	@Test
	public void getProductInfoTest() {
		List<ProductInfo> list = productInfoRepository.getProductInfo();
		assertThat(list).isNotEmpty();
		assertThat(list.get(0).getName()).isEqualTo("phone");
	}

	@Test
	public void updateProductTest() {
		productRepository.updateProduct("smartphone", 1L);
		Optional<Product> product = productRepository.findById(1L);
		assertThat(product.get().getName()).isEqualTo("smartphone");
	}

	@Test
	public void updateProductInfoTest() {
		productInfoRepository.updateProductInfo("cellphone", 1L);
		Optional<Product> product = productRepository.findById(1L);
		assertThat(product.get().getName()).isEqualTo("cellphone");
	}

}

Run Junit Tests

Primary Datasource JUnit Tests Results

Primary Data Source JUnit Test Results

Secondary Datasource JUnit Test Results

Secondary Data Source JUnit Test Results

Source Code

https://github.com/JavaChinna/spring-boot-multiple-datasources

Conclusion

That’s all folks. In this article, we have configured multiple data sources with our Spring Boot application.

Thank you for reading.

Leave a Reply