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.
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
Secondary Datasource 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.