系列文章目录(兼容Oracle与MySQL)
一个系统要兼容多种数据库应该是很多系统都要面对的问题。曾经,Hibernate
作为数据库层的王者,风光了十几年,说实在话,它在兼容多种数据库方面确实方便且功能强大,通过方言(dialect
)就可以了。但是,目前笨重的Hibernate
已经渐渐走出了历史舞台,MyBatis
以轻巧性能高成为数据层的事实框架,而且扩展也非常之多。所有本章主要只会涉及到MyBatis
中的相关知识。本章可参考源码。
提示:以下是本篇文章正文内容,下面案例可供参考
如果说MyBatis没有考虑兼容多数据库,那么是不可能的。在官方文档数据库厂商标识(databaseIdProvider)
这里介绍了大致的使用方式,但是不全,尤其是在不使用MyBatis配置文件与Spring整合时的使用方式。
首先需要实现接口org.apache.ibatis.mapping.DatabaseIdProvider
来提供数据信息,注意,这里不是要告诉MyBatis你当前使用的是哪个数据库,而是数据库厂商名称与在你们的项目中数据库标识的映射信息。比如配置一个Bean如下
@Bean
public DatabaseIdProvider databaseIdProvider() {
DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
Properties properties = new Properties();
properties.setProperty("MySQL", "mysql");
properties.setProperty("Oracle", "oracle");
databaseIdProvider.setProperties(properties);
return databaseIdProvider;
}
这里配置了两个映射关系,其中的key称为数据库厂商标识
,可以通过JDBC规范查询。
// 获取数据库连接池
DataSource dataSource=...
// 获取数据库连接
Connection con = dataSource.getConnection();
// 获取连接元数据
DatabaseMetaData metaData = con.getMetaData();
// 获取数据库厂商标识
return metaData.getDatabaseProductName();
如果是MySQL数据库返回的就是MySQL,而Oracle数据库就是Oracle.
但是在MyBatis中写mapper.xml文件(statement语句)的时候你就未必写MySQL、Oracle了。比如
<mapper namespace="com.example.durid.demo.mapper.TtrdTestBaseidMapper">
<resultMap id="BaseResultMap" type="com.example.durid.demo.entity.TtrdTestBaseid">
<id column="BASEID" jdbcType="DECIMAL" property="baseid"/>
<result column="NAME" jdbcType="VARCHAR" property="name"/>
resultMap>
<select id="selectAll" resultMap="BaseResultMap" databaseId="mysql">
select BASEID, NAME
from TTRD_TEST_BASEID
where BASEID = 1
select>
<select id="selectAll" resultMap="BaseResultMap" databaseId="oracle">
select BASEID, NAME
from TTRD_TEST_BASEID
where BASEID = 2
select>
mapper>
在上面这里,我们定义了id都为selectAll
的statement语句,但是databaseId
是不同的。这里的databaseId
用于指定当前的语句所适用的数据库。上面的DatabaseIdProvider
定义的就是数据库厂商标识和这里配置的databaseId
的映射关系。
那么以上这种机制是如何作用的呢?以及作用的时机?如果要使用MyBatis,首先就需要创建SqlSessionFactory
这个接口的对象,其实也就是org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
。里面包含着org.apache.ibatis.session.Configuration
属性,这个配置类其实代表的是mybatis-config.xml
配置文件信息,从这个配置文件加载到数据源、拦截器、插件、别名等等,当然包括所有的mapper.xml文件,最后进行解析。在与Spring整合时是在SqlSessionFactoryBean
类型Bean初始化的时候进行的(org.mybatis.spring.SqlSessionFactoryBean#afterPropertiesSet
).在这个解析的过程中,MyBatis根据数据库连接池获取到数据库厂商标识,然后按照映射关系过滤掉databaseId不匹配的statement语句,最后都放到org.apache.ibatis.session.Configuration#mappedStatements
这个Map
类型的属性当中。当业务请求的时候,调用TtrdTestBaseidMapper#selectAll
接口的时候,只会映射到一个语句了。如果存在多个,首先根据databaseId匹配,然后在匹配没有databseId的,已经有匹配成功的则忽略。
相关源码为:
1. org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory
if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
try {
targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
} catch (SQLException e) {
throw new NestedIOException("Failed getting a databaseId", e);
}
}
2.org.apache.ibatis.mapping.VendorDatabaseIdProvider#getDatabaseId
@Override
public String getDatabaseId(DataSource dataSource) {
if (dataSource == null) {
throw new NullPointerException("dataSource cannot be null");
}
try {
return getDatabaseName(dataSource);
} catch (Exception e) {
LogHolder.log.error("Could not get a databaseId from dataSource", e);
}
return null;
}
3. 从配置文件中查找
private String getDatabaseName(DataSource dataSource) throws SQLException {
String productName = getDatabaseProductName(dataSource);
if (this.properties != null) {
for (Map.Entry<Object, Object> property : properties.entrySet()) {
if (productName.contains((String) property.getKey())) {
return (String) property.getValue();
}
}
// no match, return null
return null;
}
return productName;
}
从这里可以看出,如果properties
没有任何配置的话,则使用的就是数据库厂商名称。也就是说databaseIdProvider
这个bean其实可以不用配的,只需要在mapper.xml文件中databaseId严格按照厂商名称来即可。
4. 构建Statement,根据dataBaseId过滤
org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
/**
* 如果系统有配置DatabaseId 则requiredDatabaseId为系统配置的值或者数据厂商标识
* 如果没有与配置匹配上 则为null
*/
private void buildStatementFromContext (List < XNode > list, String requiredDatabaseId){
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
```重点```:首先按照requiredDatabaseId加载一遍。然后按照null加载一遍。如果先匹配了requiredDatabaseId,按照null就不会匹配了(databaseIdMatchesCurrent方法中会判断的)。按照官方的说法: MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。 如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 进行databaseId匹配
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// ...
}
最终匹配方法如下
private boolean databaseIdMatchesCurrent (String id, String databaseId, String requiredDatabaseId){
if (requiredDatabaseId != null) {
return requiredDatabaseId.equals(databaseId);
}
if (databaseId != null) {
return false;
}
id = builderAssistant.applyCurrentNamespace(id, false);
if (!this.configuration.hasStatement(id, false)) {
return true;
}
// skip this statement if there is a previous one with a not null databaseId
MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2
return previous.getDatabaseId() == null;
}
其中id
为当前statement语句的主键, databaseId
为当前语句的databaseId属性, requiredDatabaseId
为经过MyBatis解析的目前的databaseId,假设当前数据源为MySQL
根据上面第三步getDatabaseName的结果:
如果没有配置databaseIdProvider
的话,不会进入getDatabaseName方法,requiredDatabaseId此时为null,结果如下(只会匹配那些不包含databaseId的语句):
如果有配置databaseIdProvider
,但是没有配置映射关系,那么requiredDatabaseId就是MySQL
,此时databaseId必须完全匹配
@Bean
public DatabaseIdProvider databaseIdProvider() {
return new VendorDatabaseIdProvider();
}
databaseIdProvider
而且有映射关系MySQL="mysql
,那么requiredDatabaseId就是mysql
,此时databaseId必须完全匹配,情况与上面相似,只是requiredDatabaseId不同而已,需要修改xml文件匹配。databaseIdProvider
但是是其他映射关系DB2="db2
,那么requiredDatabaseId也是null,与第一种情况相同所以按照如下的方式,总能匹配上一个(null匹配最后替补):
<select id="selectAll" resultMap="BaseResultMap" databaseId="MySQL">
select BASEID, NAME
from TTRD_TEST_BASEID
where BASEID = 1
</select>
<select id="selectAll" resultMap="BaseResultMap" databaseId="Oracle">
select BASEID, NAME
from TTRD_TEST_BASEID
where BASEID = 2
</select>
<select id="selectAll" resultMap="BaseResultMap">
select BASEID, NAME
from TTRD_TEST_BASEID
where BASEID = 3
</select>
当然databaseId不仅仅是使用在select
类型的statement当中,还可以在Insert, Update, Delete
这些语句当中。比如下面这种方式(不同数据库的获取主键方式可能不同):
org.apache.ibatis.builder.xml.XMLStatementBuilder#processSelectKeyNodes
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
removeSelectKeyNodes(selectKeyNodes);
}
如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句。
参考官方文档
<insert id="insert">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
<if test="_databaseId == 'oracle'">
select seq_users.nextval from dual
if>
<if test="_databaseId == 'db2'">
select nextval for seq_users from sysibm.sysdummy1"
if>
selectKey>
insert into users values (#{id}, #{name})
insert>
比如我们按照如下配置
<mapper namespace="com.example.durid.demo.mapper.TtrdTestBaseidMapper">
<resultMap id="BaseResultMap" type="com.example.durid.demo.entity.TtrdTestBaseid">
<id column="BASEID" jdbcType="DECIMAL" property="baseid"/>
<result column="NAME" jdbcType="VARCHAR" property="name"/>
resultMap>
<select id="selectAll" resultMap="BaseResultMap">
select BASEID, NAME
from TTRD_TEST_BASEID
<if test="_databaseId == 'mysql'">
where BASEID = 1
if>
<if test="_databaseId == 'oracle'">
where BASEID = 2
if>
select>
mapper>
在MySQL数据环境中
在Oracel数据环境中
执行查询语句org.apache.ibatis.executor.CachingExecutor#query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
在org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
。首先需要构造DynamicContext
这个时候就会设置相关参数_databaseId
.
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
bindings = new ContextMap(metaObject, existsTypeHandler);
} else {
bindings = new ContextMap(null, false);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
// 读取DatabaseId值
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
执行时进行ognl语句匹配,此时会读取DynamicContext
中配置值。
另外通常在程序中大家可能按照如下模式来配置
<select id="selectAll" resultMap="BaseResultMap">
select BASEID, NAME
from TTRD_TEST_BASEID
<if test="'${dialect}' == 'mysql'">
where BASEID = 1
if>
<if test="'${dialect}' == 'oracle'">
where BASEID = 2
if>
select>
这里的变量不是Spring的系统变量,也不是系统的变量,而是MyBatis的变量,说的更准备一点就是org.apache.ibatis.session.Configuration
对象的variables
属性。官方配置方法参考。
如果是普通Spring项目设置方式为org.mybatis.spring.SqlSessionFactoryBean#setConfigurationProperties
.
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
Properties configurationProperties = new Properties();
configurationProperties.put("dialect", SystemInitConfigUtils.getDialect());
// isordidstring=true
configurationProperties.put("isordidstring", "true");
factory.setConfigurationProperties(configurationProperties);
String[] mapperLocations = SystemInitConfigUtils.getMybatisMapperLocation();
factory.setMapperLocations(ResourceLoaderUtil.resolveMapperLocations(mapperLocations));
return factory.getObject();
}
如果是SpringBoot项目只要在配置文件中添加如下配置即可
mybatis.configuration-properties.dialect=mysql
配置这个参数,会调用org.mybatis.spring.boot.autoconfigure.MybatisProperties#setConfigurationProperties
设置参数,然后再org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory
的时候进行设置。
targetConfiguration = this.configuration;
if (targetConfiguration.getVariables() == null) {
targetConfiguration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
targetConfiguration.getVariables().putAll(this.configurationProperties);
}
很多人误以为这个参数设置后,在发起请求时会动态读取这个参数用于匹配。其实是错误的,当MyBatis启动完之后,再修改这个参数没有任何用的,因为在启动过程中已经完成了解析,将xml文件中的变量都进行了替换。
解析org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
,将动态标签解析成MixedSqlNode
对象
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
// 获取节点的子节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 遍历所有子节点
XNode child = node.newXNode(children.item(i));
// 不同子节点类型的解析 主要是xml规范
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// 解析动态SQL部分
String nodeName = child.getNode().getNodeName();
XMLScriptBuilder.NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
通过当前节点创建newXNode节点的时候,
public XNode newXNode(Node node) {
return new XNode(xpathParser, node, variables);
}
public XNode(XPathParser xpathParser, Node node, Properties variables) {
this.xpathParser = xpathParser;
this.node = node;
this.name = node.getNodeName();
this.variables = variables;
// 解析并替换变量
this.attributes = parseAttributes(node);
this.body = parseBody(node);
}
private Properties parseAttributes(Node n) {
Properties attributes = new Properties();
NamedNodeMap attributeNodes = n.getAttributes();
if (attributeNodes != null) {
for (int i = 0; i < attributeNodes.getLength(); i++) {
Node attribute = attributeNodes.item(i);
String value = PropertyParser.parse(attribute.getNodeValue(), variables);
attributes.put(attribute.getNodeName(), value);
}
}
return attributes;
}
通过org.apache.ibatis.parsing.PropertyParser
根据参数的节点值和MyBatis配置参数解析完成的。
实际执行MyBatis查询流程为首先需要根据接口查找对应的MappedStatement
,再根据MappedStatement
以及外面传入的参数构建BoundSql
(查询语句和参数的包装类)。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
其实就是一个一个sql节点的拼接(具体源码参考org.apache.ibatis.mapping.MappedStatement#getBoundSql
)
注意不能写成如下格式(引用变量少了单引号)
"${dialect}!='mysql'"
否则会抛出异常
2020-11-04 11:33:53.992 ERROR 8516 --- [nio-8083-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression '${dialect} == 'mysql''. Cause: org.apache.ibatis.ognl.ExpressionSyntaxException: Malformed OGNL expression: ${dialect} == 'mysql' [org.apache.ibatis.ognl.ParseException: Encountered " "$" "$ "" at line 1, column 1.
Was expecting one of:
":" ...
"not" ...
"+" ...
"-" ...
"~" ...
"!" ...
"(" ...
"true" ...
什么是动态多数据源呢?多数据源,就是这个系统可以支持多种数据源,但是动态,就要求启动之后,能随时切换数据源。比如通过前端请求随时切换数据源,可以是MySQL,也可以是Oracle。在Spring中提供了一个抽象类AbstractRoutingDataSource
,通过继承该类实现determineCurrentLookupKey
方法来动态获取数据源。这个类中通过targetDataSources保存多个数据源信息,其中key作为标识,value就是DataSource
。用户继承这个类创建的bean实例化的过程中会解析各个DataSource
并存放到resolvedDataSources
中。然后每次数据库请求时调用determineTargetDataSource
,这个时候只要用户实现的determineCurrentLookupKey
这个方法返回的值不同,就会到resolvedDataSources
中获取到不同的数据源了,实现动态数据源的切换。
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Map<Object, DataSource> resolvedDataSources;
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
为了标识不同的数据源
public enum DataSourceTypeEnum {
/**
* oracle数据库
*/
ORACLE("oracle"),
/**
* mysql数据库
*/
MYSQL("mysql");
String value;
DataSourceTypeEnum(String value) {
this.value = value;
}
public static DataSourceTypeEnum getEnum(String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException("value值不允许为空");
}
DataSourceTypeEnum[] values = values();
for (DataSourceTypeEnum dataSourceTypeEnum : values) {
if (dataSourceTypeEnum.value.equalsIgnoreCase(value)) {
return dataSourceTypeEnum;
}
}
throw new IllegalArgumentException("不存在的枚举值" + value);
}
public String getValue() {
return value;
}
}
创建一个类方便获取这个标识和设置标识(注意必须线程安全)
public class DataSouceTypeContext {
private static final ThreadLocal<DataSourceTypeEnum> dataSouceKeyHolder = new ThreadLocal<>();
public static void set(DataSourceTypeEnum dataSourceType) {
dataSouceKeyHolder.set(dataSourceType);
}
/**
* 默认值为ORACLE
*
* @return 设置的数据库类型
*/
public static DataSourceTypeEnum get() {
return dataSouceKeyHolder.get() != null ? dataSouceKeyHolder.get() : DataSourceTypeEnum.ORACLE;
}
public static void clear() {
dataSouceKeyHolder.remove();
}
}
实现一个动态数据源如下,其中determineCurrentLookupKey
这个方法就是从DataSouceTypeContext
读取数据库标识。
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
DataSourceTypeEnum dataSourceTypeEnum = DataSouceTypeContext.get();
logger.info("当前数据源为{}", dataSourceTypeEnum.getValue());
return dataSourceTypeEnum.getValue();
}
}
完成以上配置的时候,只要在控制层接收请求的时候,根据前台参数修改DataSouceTypeContext信息就可以实现动态数据源了。
此时该如何与多数据源结合呢?通过databaseIdProvider+databaseId方式是不行的,因为在系统启动的时候,虽然DynamicDataSource
内部有多个数据源,但此时通过determineCurrentLookupKey
返回的那个默认的数据源会作为databaseId,解析过程中就把其他数据源的配置信息过滤了,谈不上执行时再动态匹配的问题,而MyBatis变量+动态SQL方式在解析的时候已经把相关变量替换成了用户设置的值,执行的时候即使修改了MyBatis变量也没有用。此时只有第二种方法才是可以的,为什么呢?
在org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
方法中
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
会每次根据configuration和parameterObject构造DynamicContext
,上面已经说过,此时会读取databaseId值设置到_databaseId属性当中,所以只要在切换数据源的时候,再去修改configuration中的databaseId属性即可。通过SqlSessionFactory这个Bean获取Configuration对象,然后设置就好了。
比如按照如下的方式
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class SqlSessionFacotoryUtils {
private static SqlSessionFactory sqlSessionFactory;
public static void setDataBaseId() {
// 读取动态数据源标识并设置到mybatis环境中
sqlSessionFactory.getConfiguration().setDatabaseId(DataSouceTypeContext.get().getValue());
}
@EventListener
public void contextRefresh(ContextRefreshedEvent contextRefreshedEvent) {
// 监听事件 获取sqlSessionFactory变量
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
}
}
根据前端请求切换数据源
package com.example.durid.demo.interceptor;
import com.example.durid.demo.config.DataSouceTypeContext;
import com.example.durid.demo.config.DataSourceTypeEnum;
import com.example.durid.demo.config.SqlSessionFacotoryUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class DataBaseChooseHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String db = request.getParameter("db");
// 如果前端请求当中包含db参数,则切换数据源
if (StringUtils.isNotBlank(db)) {
DataSourceTypeEnum dataSourceTypeEnum = DataSourceTypeEnum.getEnum(db);
DataSouceTypeContext.set(dataSourceTypeEnum);
}
// 切换数据源之后 设置databaseId
SqlSessionFacotoryUtils.setDataBaseId();
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
DataSouceTypeContext.clear();
SqlSessionFacotoryUtils.setDataBaseId();
}
}
比较以上三种方式:
第一种:databaseIdProvider结合xml中使用databaseId的方式,是在解析过程中完成过滤的,执行过程中不需要再进行判断
第二种:官方动态SQL方式,在真正执行语句的时候读取databaseId值,然后通过ognl语句匹配的。所以执行过程中需要匹配
第三种:MySQL变量+动态SQL方式,是在解析过程中完成占位符解析替换变量的,真正语句执行时再通过ognl进行动态判断的。
其中能够搭配动态多数据源的只有第二种方式。但要注意一点,在MyBatis中无论SqlSessionFactory还是Configuration都是单例的,也就是说针对Configuration里面参数的修改会影响到所有的数据库请求,因为在切换数据库之前必须保证其他类型数据库的请求已经完成,否则会导致错误的查询。