spring boot mybatis 多数据源

在实际开发中,我们一个项目可能会用到多个数据库,通常一个数据库对应一个数据源。

在spring boot项目中,系统默认会自动在applicationContext中注册一个dataSource的bean,如果我们自己定义一个DataSource.class的实例,则会覆盖这个bean。但是如果我们定义多个DataSource.class的实例,则启动会提示实例化mapper的时候发现了多个datasource,导致启动失败。

我们先来看看单数据源的配置案例:

1、单数据源情况

1.1、MyBatisConfiguration

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@ConditionalOnClass({EnableTransactionManagement.class})
@MapperScan(basePackages={"com.roy.**.mapper"})
public class MyBatisConfiguration {

    @Autowired
    private DataSource dataSource;

    public DataSource dataSource() {
        return dataSource;
    }

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean() throws Exception {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource());

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/mybatis/**/*.xml"));
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }


}

1.2、application.properties

# 数据库访问配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@//127.0.0.1:1521/testdb
spring.datasource.username=test
spring.datasource.password=test
# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.filters=stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
#spring.datasource.useGlobalDataSourceStat=true

下面我们再看看如何改造成多数据源
多数据源的主要实现原理是重写DataSource接口的实现,重写getConnection()和unwrap()方法,在这里实现对多数据源datasource的选择切换,并注册给SqlSessionFactory和PlatformTransactionManager
我们参考AbstractRoutingDataSource类,发现里面已经支持了路由多个datasource的功能,我们只需要实现protected abstract Object determineCurrentLookupKey();方法来切换datasource就可以。
为此我们参考网上例子,对上面的单数据源做如下调整,以支持多数据源。

2、多数据源情况

2.1、新建DynamicDataSource类继承AbstractRoutingDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getDatabaseName();
    }
}

2.2、新建DatabaseContextHolder,利用线程变量保存当前数据源的key值(此处我们使用dataSource实例的beanName作为key值)

public class DatabaseContextHolder {

    private static final ThreadLocal contextHolder = new ThreadLocal<>();

    public static void setDatabaseName(String type){
        contextHolder.set(type);
    }

    public static String getDatabaseName(){
         return contextHolder.get();
    }

    public static void clear() {
        contextHolder.remove();
    }

}

2.3、改造上面的MyBatisConfiguration类,重写 dataSource() 方法

    @Autowired
    private DataSource dataSource;

    public Map otherDataSources() {
        return null;
    }

    public DataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map targetDataSources = new HashMap<>();
        targetDataSources.put("dataSource", dataSource);
        if (otherDataSources()!=null) {
            for (String key : otherDataSources().keySet()) {
                targetDataSources.put(key, otherDataSources().get(key));
            }
        }
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(dataSource);
        dynamicDataSource.afterPropertiesSet();
        return dynamicDataSource;
    }

注意:这里我没有采用网上的实例化多个DataSource.class的bean到applicationContext的方式,因为经我实际验证发现,但凡applicationContext里面有多个DataSource.class的bean,生产mapper的bean的时候都会报错(也许是我的架构上不知道哪里有些限制)。所以我这里采用了特殊的方式,增加了一个
public Map otherDataSources() {
return null;
}
的方法,如果有多个DataSource,在这里面自己new 出来,并且不注入到applicationContext里面。

2.4、如何自己实例化DataSource,参考下面这个,这里我们采用DruidDataSource

    @Value("${second.datasource.url}")
    private String dbUrl;
    @Value("${second.datasource.username}")
    private String username;
    @Value("${second.datasource.password}")
    private String password;
    @Autowired
    protected DataSourceProperties dataSourceProperties;

    public static final String DATASOURCE_SECOND_KEY="secondDataSource";

    public Map otherDataSources() {
        Map map = new HashMap<>();
        map.put(DATASOURCE_SECOND_KEY, secondDataSource());
        return map;
    }

    public DataSource secondDataSource() {
        DruidDataSource datasource = new DruidDataSource();
        datasource.setUrl(dbUrl);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(dataSourceProperties.getDriverClassName());
        datasource.setInitialSize(dataSourceProperties.getInitialSize());
        datasource.setMinIdle(dataSourceProperties.getMinIdle());
        datasource.setMaxActive(dataSourceProperties.getMaxActive());
        datasource.setMaxWait(dataSourceProperties.getMaxWait());
        datasource.setTimeBetweenEvictionRunsMillis(dataSourceProperties.getTimeBetweenEvictionRunsMillis());
        datasource.setMinEvictableIdleTimeMillis(dataSourceProperties.getMinEvictableIdleTimeMillis());
        datasource.setValidationQuery(dataSourceProperties.getValidationQuery());
        if (dataSourceProperties.getTestWhileIdle()!=null) {
            datasource.setTestWhileIdle(dataSourceProperties.getTestWhileIdle());
        }
        if (dataSourceProperties.getTestOnBorrow()!=null){
            datasource.setTestOnBorrow(dataSourceProperties.getTestOnBorrow());
        }
        if (dataSourceProperties.getTestOnReturn()!=null) {
            datasource.setTestOnReturn(dataSourceProperties.getTestOnReturn());
        }
        if (dataSourceProperties.getPoolPreparedStatements()!=null) {
            datasource.setPoolPreparedStatements(dataSourceProperties.getPoolPreparedStatements());
        }
        if (dataSourceProperties.getMaxPoolPreparedStatementPerConnectionSize()!=null) {
            datasource.setMaxPoolPreparedStatementPerConnectionSize(dataSourceProperties.getMaxPoolPreparedStatementPerConnectionSize());
        }
        if (dataSourceProperties.getConnectionProperties()!=null) {
            datasource.setConnectionProperties(dataSourceProperties.getConnectionProperties());
        }
        if (dataSourceProperties.getUseGlobalDataSourceStat()!=null) {
            datasource.setUseGlobalDataSourceStat(dataSourceProperties.getUseGlobalDataSourceStat());
        }
        try {
            datasource.setFilters(dataSourceProperties.getFilters());
        } catch (SQLException e) {
            logger.error("dataSource configuration initialization filter", e);
        }
        return datasource;
    }

其中DataSourceProperties 类是注入了application.properties的spring.datasource. 的参数

2.5、application.properties里面加入第二个datasource的配置

second.datasource.url=jdbc:oracle:thin:@//127.0.0.1:1521/testdb2
second.datasource.username=test2
second.datasource.password=test2

2.6、使用方法

// 访问默认数据源
        City city = cityService.getCityById(id,null);
// 以下是访问第二个数据源
        DatabaseContextHolder.setDatabaseName(MyBatisConfiguration.DATASOURCE_SECOND_KEY);
        city = cityService.getCityById(id,null);
        DatabaseContextHolder.clear();

如上,我们在两个库都建立一张city表,都配置一条cityId=1的记录,第一个库,cityName=深圳,第二个库,cityName=洛杉矶。
经过上面的两次请求,返回的cityName结果如我们预料,说明数据源已经做了正常切换。
注意:每次切换DataSource之后记得用DatabaseContextHolder.clear();方法把线程变量清空。

后续

1、上面写的,如果applicationContext里面有多个DataSource.class的bean会导致启动时生成mapper时报错。后面发现如果在其中一个DataSource的bean上加上@Primary注解就可以了
2、以上覆盖了dataSource()方法,返回的DataSource的实例是DynamicDataSource的实例,这样会导致整个项目的事务失效,所以如果系统有需要事务的地方,要慎重使用多数据源配置,多数据源比较适合的场景是数据分析,大部分都是查询逻辑,整合不同库的数据。
3、以上方式只支持SqlSessionTemplate的查询,但是这种查询一定要对应有mapper的sqlId。如果有需求需要使用自定义的sql进行查询,大多数时候我们会使用jdbcTemplate来查询,但是此时的jdbcTemplate使用的dataSource并不是动态数据源,所以使用jdbcTemplate不能起到切换数据源的效果。为此,可以参考mybatis 最简单的执行自定义SQL语句,原理就是新建一个mapper:

List select(String sql);


parameterType为String的话 参数名就必须写_parameter,不能用#{sqlStr}这种方式,否则会有sql注入报错。

参考资料
第八章 springboot + mybatis + 多数据源
Spring Boot Druid数据源配置

你可能感兴趣的:(spring boot mybatis 多数据源)