How to Configure Master‑Slave Data Sources in Spring Boot with JPA & MyBatis
This guide walks through setting up a Spring Boot 2.1.4 application with JDK 1.8, Oracle, and HikariCP to use separate master and slave data sources, covering Maven dependencies, YAML configuration, custom property classes, JPA EntityManager factories, MyBatis SqlSession factories, mapper scanning, and sample entity definitions.
Development environment: JDK 1.8+, Spring Boot 2.1.4.RELEASE, Oracle.
Assume two data sources: master and slave .
pom.xml dependencies
<code><dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.github.noraui</groupId>
<artifactId>ojdbc7</artifactId>
<version>12.1.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies></code>application.yml configuration for master and slave data sources, MyBatis and JPA settings
<code>server:
port: 50000
---
spring:
jpa:
hibernate:
ddlAuto: update
openInView: true
showSql: false
databasePlatform: org.hibernate.dialect.Oracle10gDialect
---
# master datasource
master:
datasource:
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@localhost:1521/orcl
username: t0
password: t0
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimumIdle: 10
maximumPoolSize: 200
autoCommit: true
idleTimeout: 30000
poolName: MasterDatabookHikariCP
maxLifetime: 1800000
connectionTimeout: 30000
connectionTestQuery: SELECT 1 FROM DUAL
---
# slave datasource
slave:
datasource:
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@localhost:1521/orcl
username: t1
password: t1
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimumIdle: 10
maximumPoolSize: 200
autoCommit: true
idleTimeout: 30000
poolName: SlaveDatabookHikariCP
maxLifetime: 1800000
connectionTimeout: 30000
connectionTestQuery: SELECT 1 FROM DUAL
---
# MyBatis configuration for each datasource
master:
mybatis:
config-location: classpath:/MyBatis-conf.xml
type-aliases-package: com.pack.domain
mapper-locations:
- classpath:/com/pack/mapper/oracle/*.xml
slave:
mybatis:
config-location: classpath:/MyBatis-conf.xml
type-aliases-package: com.pack.slave.domain
mapper-locations:
- classpath:/com/pack/slave/mapper/oracle/*.xml
---
# JPA configuration for each datasource
master:
jpa:
repos: com.pack.base.repository
domain: com.pack.domain
slave:
jpa:
repos: com.pack.slave.repository
domain: com.pack.slave.domain</code>BaseDataSourceProperties class (core property handling for a datasource)
<code>public class BaseDataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
private String name;
private boolean generateUniqueName;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
private String jndiName;
private DataSourceInitializationMode initializationMode = DataSourceInitializationMode.EMBEDDED;
private String platform = "all";
private List<String> schema;
private String schemaUsername;
private String schemaPassword;
private List<String> data;
private String dataUsername;
private String dataPassword;
private boolean continueOnError = false;
private String separator = ";";
private Charset sqlScriptEncoding;
private EmbeddedDatabaseConnection embeddedDatabaseConnection = EmbeddedDatabaseConnection.NONE;
private Xa xa = new Xa();
private String uniqueName;
@Override
public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; }
@Override
public void afterPropertiesSet() throws Exception {
this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader);
}
public DataSourceBuilder<?> initializeDataSourceBuilder() {
return DataSourceBuilder.create(getClassLoader()).type(getType())
.driverClassName(determineDriverClassName()).url(determineUrl())
.username(determineUsername()).password(determinePassword());
}
// getters, setters and helper methods omitted for brevity
public static class Xa {
private String dataSourceClassName;
private Map<String, String> properties = new LinkedHashMap<>();
public String getDataSourceClassName() { return this.dataSourceClassName; }
public void setDataSourceClassName(String dataSourceClassName) { this.dataSourceClassName = dataSourceClassName; }
public Map<String, String> getProperties() { return this.properties; }
public void setProperties(Map<String, String> properties) { this.properties = properties; }
}
static class DataSourceBeanCreationException extends BeanCreationException {
private static final long serialVersionUID = 1L;
private final BaseDataSourceProperties properties;
private final EmbeddedDatabaseConnection connection;
DataSourceBeanCreationException(String message, BaseDataSourceProperties properties,
EmbeddedDatabaseConnection connection) {
super(message);
this.properties = properties;
this.connection = connection;
}
public BaseDataSourceProperties getProperties() { return this.properties; }
public EmbeddedDatabaseConnection getConnection() { return this.connection; }
}
}</code>BaseMybatisProperties class (holds MyBatis configuration)
<code>public class BaseMybatisProperties {
private String configLocation;
private String[] mapperLocations;
private String typeAliasesPackage;
private String typeHandlersPackage;
private boolean checkConfigLocation = false;
private ExecutorType executorType;
private Configuration configuration;
// getters and setters omitted for brevity
public Resource[] resolveMapperLocations() {
List<Resource> resources = new ArrayList<>();
if (this.mapperLocations != null) {
for (String mapperLocation : this.mapperLocations) {
try {
Resource[] mappers = new PathMatchingResourcePatternResolver()
.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) { }
}
}
Resource[] mapperLocations = new Resource[resources.size()];
return resources.toArray(mapperLocations);
}
}</code>Properties classes for master and slave data sources
<code>@Component
@ConfigurationProperties(prefix = "master.datasource")
public class MasterDataSourceProperties extends BaseDataSourceProperties {}
@Component
@ConfigurationProperties(prefix = "slave.datasource")
public class SlaveDataSourceProperties extends BaseDataSourceProperties {}
@Component
@ConfigurationProperties(prefix = "master.mybatis")
public class MasterMybatisProperties extends BaseMybatisProperties {}
@Component
@ConfigurationProperties(prefix = "slave.mybatis")
public class SlaveMybatisProperties extends BaseMybatisProperties {}</code>HikariDataSourceConfig – creates the master and slave Hikari data sources
<code>@Configuration
public class HikariDataSourceConfig {
@Bean
@Primary
public HikariDataSource masterDataSource(MasterDataSourceProperties properties) {
HikariDataSource ds = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) { ds.setPoolName(properties.getName()); }
return ds;
}
@Bean
public HikariDataSource slaveDataSource(SlaveDataSourceProperties properties) {
HikariDataSource ds = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) { ds.setPoolName(properties.getName()); }
return ds;
}
@SuppressWarnings("unchecked")
protected static <T> T createDataSource(BaseDataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
}</code>EntityManagerFactoryConfig – configures JPA EntityManagerFactory and transaction manager for both datasources
<code>public class EntityManagerFactoryConfig {
@Configuration
@EnableJpaRepositories(basePackages = {"${master.jpa.repos}"},
entityManagerFactoryRef = "masterEntityManagerFactory",
transactionManagerRef = "masterJPATransactionManager")
static class MasterEntityManagerFactory {
@Resource(name = "masterDataSource")
private DataSource masterDataSource;
@Value("${master.jpa.domain}")
private String masterDomainPkg;
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(EntityManagerFactoryBuilder builder) {
Map<String, Object> props = new HashMap<>();
props.put("hibernate.hbm2ddl.auto", "update");
props.put("hibernate.id.new_generator_mappings", true);
props.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
return builder.dataSource(masterDataSource)
.packages(masterDomainPkg)
.persistenceUnit("master")
.properties(props)
.build();
}
@Bean
@Primary
public PlatformTransactionManager masterJPATransactionManager(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(masterEntityManagerFactory(builder).getObject());
}
}
@Configuration
@EnableJpaRepositories(basePackages = {"${slave.jpa.repos}"},
entityManagerFactoryRef = "slaveEntityManagerFactory",
transactionManagerRef = "slaveJPATransactionManager")
@ConditionalOnProperty(name = "multiple.ds.enabled", havingValue = "true")
static class SlaveEntityManagerFactory {
@Resource(name = "slaveDataSource")
private DataSource slaveDataSource;
@Value("${slave.jpa.domain}")
private String slaveDomainPkg;
@Bean
public LocalContainerEntityManagerFactoryBean slaveEntityManagerFactory(EntityManagerFactoryBuilder builder) {
Map<String, Object> props = new HashMap<>();
props.put("hibernate.hbm2ddl.auto", "update");
props.put("hibernate.id.new_generator_mappings", true);
props.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
return builder.dataSource(slaveDataSource)
.packages(slaveDomainPkg)
.persistenceUnit("slave")
.properties(props)
.build();
}
@Bean
public PlatformTransactionManager slaveJPATransactionManager(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(slaveEntityManagerFactory(builder).getObject());
}
}
}</code>SqlSessionFactoryConfig – creates MyBatis SqlSessionFactory, SqlSessionTemplate and transaction manager for master and slave
<code>public class SqlSessionFactoryConfig {
@Configuration
static class MasterSqlSessionFactory {
@Resource
private MasterMybatisProperties properties;
@Autowired(required = false)
private Interceptor[] interceptors;
@Autowired
private ResourceLoader resourceLoader = new DefaultResourceLoader();
@Autowired(required = false)
private DatabaseIdProvider databaseIdProvider;
@Bean
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
factory.setConfiguration(this.properties.getConfiguration());
if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); }
if (this.databaseIdProvider != null) { factory.setDatabaseIdProvider(this.databaseIdProvider); }
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
return factory.getObject();
}
@Bean
public DataSourceTransactionManager masterTransactionManager(@Qualifier("masterDataSource") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
@Bean
public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sf) {
ExecutorType et = this.properties.getExecutorType();
return (et != null) ? new SqlSessionTemplate(sf, et) : new SqlSessionTemplate(sf);
}
}
@Configuration
@ConditionalOnProperty(name = "multiple.ds.enabled", havingValue = "true")
static class SlaveSqlSessionFactory {
@Resource
private SlaveMybatisProperties properties;
@Autowired(required = false)
private Interceptor[] interceptors;
@Autowired
private ResourceLoader resourceLoader = new DefaultResourceLoader();
@Autowired(required = false)
private DatabaseIdProvider databaseIdProvider;
@Bean
public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
factory.setConfiguration(this.properties.getConfiguration());
if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); }
if (this.databaseIdProvider != null) { factory.setDatabaseIdProvider(this.databaseIdProvider); }
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
return factory.getObject();
}
@Bean
public DataSourceTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
@Bean
public SqlSessionTemplate slaveSqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sf) {
ExecutorType et = this.properties.getExecutorType();
return (et != null) ? new SqlSessionTemplate(sf, et) : new SqlSessionTemplate(sf);
}
}
}</code>MapperScanConfig – registers mapper scanning for master and slave packages
<code>public class MapperScanConfig {
@Configuration
@MapperScan(basePackages = {"com.pack.base.mapper"}, sqlSessionTemplateRef = "masterSqlSessionTemplate")
static class MasterMapper {}
@Configuration
@MapperScan(basePackages = {"com.pack.slave.mapper"}, sqlSessionTemplateRef = "slaveSqlSessionTemplate")
@ConditionalOnProperty(name = "multiple.ds.enabled", havingValue = "true")
static class SlaveMapper {}
}</code>Sample entity classes used for testing
<code>@Entity
@Table(name = "T_USERS")
@Data
public class Users {
@Id
private Long id;
private String username;
private String password;
private String realName;
private String phone;
private String idNo;
@Column(length = 4000)
private String authority;
@Column(columnDefinition = "int default 0")
private Integer status = 0;
}
@Entity
@Table(name = "T_PERSON")
@Data
public class Person {
@Id
private Long id;
private String name;
private String email;
}</code>After creating the
com.pack.domainand
com.pack.slave.domainpackages with the above entity classes, start the Spring Boot application. If the tables
T_USERSand
T_PERSONare created in the master and slave Oracle databases respectively, the multi‑datasource configuration is successful.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.