One, the introduction

When it comes to multi-data sources, they are commonly used in the following two scenarios:

  • One is that the service is special and multiple libraries need to be connected. The class representative once migrated the new and old systems from SQLServer to MySQL, which involved some business operations. Common data extraction tools could not meet the business needs, so we had to do it by hand.

  • The second is the separation of database read and write. In the master-slave database architecture, the write operation falls to the master database and the read operation is handed over to the slave database to share the pressure of the master database.

The implementation of multiple data sources, from simple to complex, has a variety of scenarios.

This article will take SpringBoot(2.5.x)+Mybatis+H2 as an example to demonstrate a simple and reliable multi-data source implementation.

After reading this article, you will learn:

  1. SpringBootHow is the data source automatically configured
  2. SpringBootIn theMybatisHow is it automatically configured
  3. How are transactions used under multiple data sources
  4. Obtain a reliable multi-data source sample project

Automatic configuration of data sources

SpringBoot’s auto-configuration does almost all the work for us, just by importing dependencies

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2. 0</version>
</dependency>
Copy the code

When introduced in dependence on H2 database, DataSourceAutoConfiguration. Java will automatically configure a default data source: HikariDataSource, first stick source code:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
// load the data source configuration
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.class, DataSourceInitializationConfiguration.SharedCredentialsDataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {

   @Configuration(proxyBeanMethods = false)
   // The built-in database dependency condition does not take effect because it exists HikariDataSource by default
   @Conditional(EmbeddedDatabaseCondition.class)
   @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
   @Import(EmbeddedDataSourceConfiguration.class)
   protected static class EmbeddedDatabaseConfiguration {}@Configuration(proxyBeanMethods = false)
   @Conditional(PooledDataSourceCondition.class)
   @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
   @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
   protected static class PooledDataSourceConfiguration {
   Initialize pooled data sources: Hikari, Tomcat, Dbcp2, etc
   }
   // omit others
}
Copy the code

The principle is as follows:

Load the data source configuration

Through @ EnableConfigurationProperties DataSourceProperties. Class loading configuration information, observation DataSourceProperties class definition:

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware.InitializingBean
Copy the code

You get two pieces of information:

  1. The prefix isspring.datasource;
  2. To achieve theInitializingBeanInterface, with initialization operations.

The default embedded database connection is initialized according to the user configuration:

	@Override
	public void afterPropertiesSet(a) throws Exception {
		if (this.embeddedDatabaseConnection == null) {
			this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader); }}Copy the code

Through EmbeddedDatabaseConnection. The get method to traverse the built-in database, find the most suitable for the current environment of embedded database connection, because we introduced H2, so the return value is the enumeration of H2 database information:

public static EmbeddedDatabaseConnection get(ClassLoader classLoader) {
		for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) {
			if(candidate ! = NONE && ClassUtils.isPresent(candidate.getDriverClassName(), classLoader)) {returncandidate; }}return NONE;
	}
Copy the code

This is the idea behind SpringBoot’s Convention over Configuration. SpringBoot noticed that we had introduced an H2 database and immediately had the default connection information ready.

2. Create a data source

By default because SpringBoot built-in HikariDataSource pooling data source, so the @ Import (EmbeddedDataSourceConfiguration. Class) will not be loaded, will only initialize a HikariDataSource, The reason is that @ Conditional (EmbeddedDatabaseCondition. Class) in the current environment. This is explained in the source code comments:

/ * * * {@link Condition} to detect when an embedded {@link DataSource} type can be used.
 
 * If a pooled {@link DataSource} is available, it will always be preferred to an
 * {@codeEmbeddedDatabase}. * If there is a pooled DataSource, it will have a higher priority than EmbeddedDatabase */
static class EmbeddedDatabaseCondition extends SpringBootCondition {
// omit the source code
}
Copy the code

So the default data source is initialized by: @ Import ({DataSourceConfiguration. Hikari. Class, / / omit other}. The code is also relatively simple:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true)
static class Hikari {

   @Bean
   @ConfigurationProperties(prefix = "spring.datasource.hikari")
   HikariDataSource dataSource(DataSourceProperties properties) {
   // Create a HikariDataSource instance
      HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
      if (StringUtils.hasText(properties.getName())) {
         dataSource.setPoolName(properties.getName());
      }
      returndataSource; }}Copy the code
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
/ / in initializeDataSourceBuilder will use the default connection information
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
Copy the code
publicDataSourceBuilder<? > initializeDataSourceBuilder() {return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName())
         .url(determineUrl()).username(determineUsername()).password(determinePassword());
}
Copy the code

Default connection information is always used with the same idea: user-specified configurations are preferred, and defaults are used if the user hasn’t written, using determineDriverClassName() as an example:

public String determineDriverClassName(a) {
    // Return if driverClassName is configured
		if (StringUtils.hasText(this.driverClassName)) {
			Assert.state(driverClassIsLoadable(), () -> "Cannot load driver class: " + this.driverClassName);
			return this.driverClassName;
		}
		String driverClassName = null;
    // If a URL is configured, driverClassName is derived from the URL
		if (StringUtils.hasText(this.url)) {
			driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
		}
    // If not, fill it with the enumeration obtained when the data source configuration class was initialized
		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;
	}
Copy the code

DetermineUrl (), determineUsername(), determinePassword() are all the same.

At this point, the default HikariDataSource is automatically configured!

Mybatis is automatically configured in SpringBoot

Three, automatic configurationMybatis

To use Mybatis in Spring, you need at least a SqlSessionFactory and a Mapper interface, so Mybatis – spring-boot-starter does this for us:

  1. Automatic discovery of existingDataSource
  2. willDataSourcePassed to theSqlSessionFactoryBeanTo create and register oneSqlSessionFactoryThe instance
  3. usingsqlSessionFactoryCreate and registerSqlSessionTemplateThe instance
  4. Automatic scanningmapperAnd put them togetherSqlSessionTemplateLink up and register toSpringContainer for otherBeaninjection

Combine the source code to deepen the impression:

public class MybatisAutoConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean //1. 'DataSource' public SqlSessionFactory SqlSessionFactory (DataSource DataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); //2. Pass the DataSource to the SqlSessionFactoryBean to create and register an SqlSessionFactory instance factory.setdatasource (DataSource); // omit other... return factory.getObject(); } @Bean @ConditionalOnMissingBean //3. SqlSessionTemplate public SqlSessionTemplate SqlSessionTemplate (sqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType ! = null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } } /** * This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box, * similar to using Spring Data JPA repositories. */ //4. Automatically scan mapper, And ` SqlSessionTemplate ` link and register to ` Spring ` container for other ` Bean ` into public static class AutoConfiguredMapperScannerRegistrar Implements BeanFactoryAware ImportBeanDefinitionRegistrar {/ / omit the other... }}Copy the code

A picture is worth a thousand words, and its essence is to inject layers upon layers:

Four, from single to more

Armed with the knowledge of two or three summaries, there is a theoretical basis for creating multiple data sources: do two setsDataSource, two sets of layer injection, as shown in the figure:

Next, we will follow the routine of automatically configuring a single data source to configure multiple data sources, in the following order:

Datasource = spring. Datasource = yML = yML

spring:
  datasource:
    first:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:mem:db1
      username: sa
      password:
    second:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:mem:db2
      username: sa
      password:
Copy the code

First Data source configuration

/ * * *@description:
 * @author:Java class representative *@createTime: 2021/11/3 moreover, * /
@Configuration
// Set the scan location of mapper and specify the corresponding sqlSessionTemplate
@MapperScan(basePackages = "top.javahelper.multidatasources.mapper.first", sqlSessionTemplateRef = "firstSqlSessionTemplate")
public class FirstDataSourceConfig {

    @Bean
    @Primary
    // Read the configuration and create the data source
    @ConfigurationProperties(prefix = "spring.datasource.first")
    public DataSource firstDataSource(a) {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    / / create a SqlSessionFactory
    public SqlSessionFactory firstSqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // Set the scan path for XML
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/first/*.xml"));
        bean.setTypeAliasesPackage("top.javahelper.multidatasources.entity");
        org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
        config.setMapUnderscoreToCamelCase(true);
        bean.setConfiguration(config);
        return bean.getObject();
    }

    @Bean
    @Primary
    / / create SqlSessionTemplate
    public SqlSessionTemplate firstSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    @Primary
    / / create DataSourceTransactionManager for transaction management
    public DataSourceTransactionManager firstTransactionManager(DataSource dataSource) {
        return newDataSourceTransactionManager(dataSource); }}Copy the code

The @primary is added to each @bean to make it the default Bean, and the SqlSessionTemplate is specified when used by @mapperscan to associate the mapper with the firstSqlSessionTemplate.

Tip:

Finally, as the data source created a DataSourceTransactionManager, used in transaction management, Transactional(transactionManager = “firstTransactionManager”) is used to specify which transactionManager to use when using a transaction in a multi-source scenario.

At this point, the first data source is configured, and the second data source is also configured with these items. Since the beans configured are of the same type, @qualifier is used to qualify the loaded beans, for example:

@Bean
/ / create SqlSessionTemplate
public SqlSessionTemplate secondSqlSessionTemplate(@Qualifier("secondSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
}
Copy the code

The full code can be found on GitHub

Transactions under multiple data sources

Spring provides us with an easy-to-use declarative transaction that allows us to focus on business development, but it is not always easy to use right. This article focuses on multiple data sources.

As mentioned in the previous tip, you need to specify which transaction manager to use when you enable declarative transactions because there are multiple transaction managers, as in the following example:

// If transactionManager is not explicitly specified, firstTransactionManager set to Primary will be used
Firstusermapper. insert, secondUsermapper. insert(user2); Will insert normally
@Transactional(rollbackFor = Throwable.class,transactionManager = "firstTransactionManager")
public void insertTwoDBWithTX(String name) {
    User user = new User();
    user.setName(name);
    / / rollback
    firstUserMapper.insert(user);
    / / not rolled back
    secondUserMapper.insert(user);

    // Initiate rollback
    int i = 1/0;
}
Copy the code

The transaction uses firstTransactionManager as the transaction manager by default and only controls the FristDataSource transaction, so when we manually throw an exception internally to roll back the transaction, firstUsermapper.insert (user); Rollback, secondUserMapper. Insert (user); Don’t roll back.

Framework code has been uploaded, friends can design use case verification according to their own ideas.

Six, review

At this point, the SpringBoot+Mybatis+H2 multi-data source sample demonstrates that this should be the most basic multi-data source configuration, in fact, it is rarely used online, except for extremely simple one-off services.

The drawback is obvious: the code is too intrusive! As many data sources as there are to implement as many sets of components, the amount of code increases exponentially.

To write this case is to summarize and review SpringBoot automatic configuration, annotated declaration Bean, Spring declarative transaction and other basic knowledge, paving the way for the later multi-data source advancement.

Spring provides us with an official AbstractRoutingDataSource class, routed through the DataSource, realization of multiple source switching. This is the underpinning of most lightweight multi-data source implementations today.

Focus on class representative, under a demo multiple data sources based on AbstractRoutingDataSource + AOP implementation!

Seven, reference

mybatis-spring

mybatis-spring-boot-autoconfigure

Class on behalf of GitHub