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注入报错。