What if Spring and Mybatis use different data sources?

Posted by crusty76 on Sun, 26 Dec 2021 09:55:11 +0100

One of the issues to be discussed in this article is how to set up different database data sources for Spring and Mybatis?

be careful. Under normal circumstances, you must set the same database data source for Spring and Mybatis

Case code location https://github.com/infuq/spring-framework/tree/main/infuq-t/src/main/java/com/infuq/mybatis

Case code structure

//AppConfig.java

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@EnableTransactionManagement
@MapperScan("com.infuq.mybatis.mapper")
@ComponentScan
public class AppConfig {


    // data source
    @Bean
    public DataSource druidDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test_0?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true");
        dataSource.setUsername("root");
        dataSource.setPassword("9527");

        return dataSource;
    }

    // data source
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test_1?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true");
        dataSource.setUsername("root");
        dataSource.setPassword("9527");

        return dataSource;
    }

    // Mybatis needs a SqlSessionFactory, so inject a SqlSessionFactory into the container
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();

    }

    // Transaction manager for transaction management
    @Bean
    public DataSourceTransactionManager druidTransactionManager(DataSource druidDataSource) {
        return new DataSourceTransactionManager(druidDataSource);
    }

}

Graphically describe the above AppConfig Structure of Java code

The two database data sources are set to SqlSessionFactory and transaction manager respectively
SqlSessionFactory is used when Mybatis operates the database, such as insert,update, etc
The transaction manager is used to open transactions in Spring

// UserServiceImpl.java

import com.infuq.mybatis.mapper.UserMapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.transaction.annotation.Transactional;

public class UserServiceImpl implements UserService {

	@Autowired
	private UserMapper userMapper;
	
	@Transactional(transactionManager = "druidTransactionManager")
	@Override
	public void getList() {
	    userMapper.getList();
	}

}

The transaction manager is used in the code, but select is used as a case explanation instead of insert/update. Readers should not care too much

After the program runs, take a look at the UserServiceImpl instance and UserMapper instance that exist in the Spring container

The proxy object of Service is stored in the container. There is a real proxy object (i.e. a real UserServiceImpl instance) in the proxy object. Inside the proxy object, there is a mapper proxy object. The mapper proxy object holds the sqlSessionFactory object and sqlSessionFactory holds the data source

There is also a transaction interceptor TransactionInterceptor inside the proxy object of the Service

On the call link, a transaction interceptor will be called between the Service proxy object and the Service proxy object

Start running program


After running the program, first call the service proxy object, and then call the transaction interceptor. In this transaction interceptor, we get the transaction manager in the container, TransactionManager, which we configured earlier

//AppConfig.java
@Bean
public DataSourceTransactionManager druidTransactionManager(DataSource druidDataSource) {
    return new DataSourceTransactionManager(druidDataSource);
}

The transaction manager has a very important thing to do It needs to get a database connection and start a transaction
So where does the database connection come from?
When configuring the transaction manager, set a data source for it, and the transaction manager will get a database connection from this data source And it is implemented through ThreadLocal If a thread uses multiple database data sources during execution, the relationship between a data source and a database connection will be saved in ThreadLocal to ensure that the thread will only use the same database connection when operating a database The specific implementation is at org springframework. jdbc. datasource. DataSourceTransactionManager#doBegin

@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
	DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
	Connection con = null;

	try {
		if (!txObject.hasConnectionHolder() ||
				txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
			// Get the data source set in the transaction manager and create a database connection based on the data source
			Connection newCon = obtainDataSource().getConnection();
			txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
		}

		txObject.getConnectionHolder().setSynchronizedWithTransaction(true);

		con = txObject.getConnectionHolder().getConnection();

		Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
		txObject.setPreviousIsolationLevel(previousIsolationLevel);
		txObject.setReadOnly(definition.isReadOnly());


		if (con.getAutoCommit()) {
			txObject.setMustRestoreAutoCommit(true);
			// Open transaction
			con.setAutoCommit(false);
		}

		prepareTransactionalConnection(con, definition);
		txObject.getConnectionHolder().setTransactionActive(true);

		int timeout = determineTimeout(definition);
		if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
			txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
		}

		if (txObject.isNewConnectionHolder()) {
		    // Save the datasource - > connection relationship to ThreadLocal
			TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
		}
	}

}

Summary Spring will put the proxy object of the Service into the container. When calling the method of the proxy object, it will first call the transaction interceptor, which will get the transaction manager in the container. The transaction manager will create a database connection and start the transaction according to the set data source At the same time, the data source - > database connection will also be saved to ThreadLocal

Next, let's look at the code logic at the Mybatis level

After layers of calls, Mybatis also needs to get the database connection for the next operation of the database So how did you get this connection?

Mybatis originally intended to get a database connection from ThreadLocal, but the data source held by mybatis does not have a corresponding database connection in ThreadLocal, and the existing data sources in ThreadLocal are placed in the transaction manager. They are not the same data source
Therefore, Mybatis needs to create a database connection according to its own data source And put it in ThreadLocal
As shown in the figure above, at the beginning of the article, different data sources are set when configuring the transaction manager and SqlSessionFactory. Finally, when the transaction manager starts A transaction, it uses the data source A to create A database connection When Mybatis actually operates the database, it uses A database connection created by data source B As A result, the connection between opening transaction and actual database operation is not the same connection

Personal site
Language bird

official account

Topics: Java Spring Spring Boot