Backend Development 21 min read

How to Configure Master‑Slave Data Sources in Spring Boot with JPA & MyBatis

This guide walks through setting up a Spring Boot 2.4.12 project with JDK 1.8, Oracle, and two data sources—master and slave—covering Maven dependencies, application.yml settings, custom property classes, HikariDataSource beans, JPA EntityManagerFactory, MyBatis SqlSessionFactory, mapper scanning, and testing entity creation to verify successful configuration.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Configure Master‑Slave Data Sources in Spring Boot with JPA & MyBatis

Development Environment

JDK 1.8+, Spring Boot 2.4.12, Oracle.

Maven Dependencies

<code>&lt;dependencies&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;com.github.noraui&lt;/groupId&gt;
    &lt;artifactId&gt;ojdbc7&lt;/artifactId&gt;
    &lt;version&gt;12.1.0.2&lt;/version&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;
    &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt;
    &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt;
    &lt;version&gt;1.1.1&lt;/version&gt;
  &lt;/dependency&gt;
&lt;/dependencies&gt;</code>

application.yml Configuration

<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 # master package
    mapper-locations:
      - classpath:/com/pack/mapper/oracle/*.xml
slave:
  mybatis:
    config-location: classpath:/MyBatis-conf.xml
    type-aliases-package: com.pack.slave.domain # slave package
    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>

Custom Property Classes

<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 and setters omitted for brevity
    public String determineDriverClassName() {
        if (StringUtils.hasText(this.driverClassName)) {
            Assert.state(driverClassIsLoadable(), () -> "Cannot load driver class: " + this.driverClassName);
            return this.driverClassName;
        }
        String driverClassName = null;
        if (StringUtils.hasText(this.url)) {
            driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
        }
        if (!StringUtils.hasText(driverClassName)) {
            driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
        }
        if (!StringUtils.hasText(driverClassName)) {
            throw new DataSourceBeanCreationException("Failed to determine a suitable driver class", this, this.embeddedDatabaseConnection);
        }
        return driverClassName;
    }
    private boolean driverClassIsLoadable() {
        try { ClassUtils.forName(this.driverClassName, null); return true; }
        catch (UnsupportedClassVersionError ex) { throw ex; }
        catch (Throwable ex) { return false; }
    }
    public String determineUrl() {
        if (StringUtils.hasText(this.url)) { return this.url; }
        String databaseName = determineDatabaseName();
        String url = (databaseName != null) ? this.embeddedDatabaseConnection.getUrl(databaseName) : null;
        if (!StringUtils.hasText(url)) {
            throw new DataSourceBeanCreationException("Failed to determine suitable jdbc url", this, this.embeddedDatabaseConnection);
        }
        return url;
    }
    public String determineDatabaseName() {
        if (this.generateUniqueName) {
            if (this.uniqueName == null) { this.uniqueName = UUID.randomUUID().toString(); }
            return this.uniqueName;
        }
        if (StringUtils.hasLength(this.name)) { return this.name; }
        if (this.embeddedDatabaseConnection != EmbeddedDatabaseConnection.NONE) { return "testdb"; }
        return null;
    }
    // other getters/setters 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>
<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;
    public String getConfigLocation() { return this.configLocation; }
    public void setConfigLocation(String configLocation) { this.configLocation = configLocation; }
    @Deprecated public String getConfig() { return this.configLocation; }
    @Deprecated public void setConfig(String config) { this.configLocation = config; }
    public String[] getMapperLocations() { return this.mapperLocations; }
    public void setMapperLocations(String[] mapperLocations) { this.mapperLocations = mapperLocations; }
    public String getTypeHandlersPackage() { return this.typeHandlersPackage; }
    public void setTypeHandlersPackage(String typeHandlersPackage) { this.typeHandlersPackage = typeHandlersPackage; }
    public String getTypeAliasesPackage() { return this.typeAliasesPackage; }
    public void setTypeAliasesPackage(String typeAliasesPackage) { this.typeAliasesPackage = typeAliasesPackage; }
    public boolean isCheckConfigLocation() { return this.checkConfigLocation; }
    public void setCheckConfigLocation(boolean checkConfigLocation) { this.checkConfigLocation = checkConfigLocation; }
    public ExecutorType getExecutorType() { return this.executorType; }
    public void setExecutorType(ExecutorType executorType) { this.executorType = executorType; }
    public Configuration getConfiguration() { return this.configuration; }
    public void setConfiguration(Configuration configuration) { this.configuration = configuration; }
    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) { /* ignore */ }
            }
        }
        Resource[] result = new Resource[resources.size()];
        return resources.toArray(result);
    }
}
</code>

DataSource Property Beans

<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>

Hikari DataSource Configuration

<code>@Configuration
public class HikariDataSourceConfig {
    @Bean
    @Primary
    public HikariDataSource masterDataSource(MasterDataSourceProperties properties) {
        HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
        if (StringUtils.hasText(properties.getName())) {
            dataSource.setPoolName(properties.getName());
        }
        return dataSource;
    }
    @Bean
    public HikariDataSource slaveDataSource(SlaveDataSourceProperties properties) {
        HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
        if (StringUtils.hasText(properties.getName())) {
            dataSource.setPoolName(properties.getName());
        }
        return dataSource;
    }
    @SuppressWarnings("unchecked")
    protected static <T> T createDataSource(BaseDataSourceProperties properties, Class<? extends DataSource> type) {
        return (T) properties.initializeDataSourceBuilder().type(type).build();
    }
}
</code>

JPA EntityManagerFactory Configuration

<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> properties = new HashMap<>();
            properties.put("hibernate.hbm2ddl.auto", "update");
            properties.put("hibernate.id.new_generator_mappings", true);
            properties.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
            return builder.dataSource(masterDataSource).packages(masterDomainPkg).persistenceUnit("master")
                .properties(properties).build();
        }
        @Bean
        @Primary
        public PlatformTransactionManager masterJPATransactionManager(EntityManagerFactoryBuilder builder) {
            JpaTransactionManager tm = new JpaTransactionManager(masterEntityManagerFactory(builder).getObject());
            return tm;
        }
    }
    @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> properties = new HashMap<>();
            properties.put("hibernate.hbm2ddl.auto", "update");
            properties.put("hibernate.id.new_generator_mappings", true);
            properties.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
            return builder.dataSource(slaveDataSource).packages(slaveDomainPkg).persistenceUnit("slave")
                .properties(properties).build();
        }
        @Bean
        public PlatformTransactionManager slaveJPATransactionManager(EntityManagerFactoryBuilder builder) {
            JpaTransactionManager tm = new JpaTransactionManager(slaveEntityManagerFactory(builder).getObject());
            return tm;
        }
    }
}
</code>

MyBatis SqlSessionFactory Configuration

<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(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 dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
        @Bean
        public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
            ExecutorType executorType = this.properties.getExecutorType();
            return (executorType != null) ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
        }
    }
    @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(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 dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
        @Bean
        public SqlSessionTemplate slaveSqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
            ExecutorType executorType = this.properties.getExecutorType();
            return (executorType != null) ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
        }
    }
}
</code>

Mapper Scanning Configuration

<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>

Entity Classes 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;
}
</code>
<code>@Entity
@Table(name = "T_PERSON")
@Data
public class Person {
    @Id
    private Long id;
    private String name;
    private String email;
}
</code>

Testing the Configuration

Create the packages com.pack.domain and com.pack.slave.domain , add the Users and Person entities respectively, start the application under different users, and verify that the corresponding tables are created in Oracle.

All configuration steps are now complete.

Spring BootMyBatisHikariCPOracleJPAmultiple data sources
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.