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<String> 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<String, DataSource> otherDataSources() {
        return null;
    }

    public DataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> 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<String, DataSource> 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<String, DataSource> otherDataSources() {
        Map<String, DataSource> 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<map> select(String sql);

<select id="select" resultType="java.util.Map" parameterType="java.lang.String" >
    ${_parameter}
  </select>

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,812评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,626评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,144评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,052评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,925评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,035评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,461评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,150评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,413评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,501评论 2 307
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,277评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,159评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,528评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,868评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,143评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,407评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,615评论 2 335

推荐阅读更多精彩内容