兼容Oracle与MySQL的那些事

系列文章目录

系列文章目录(兼容Oracle与MySQL)

文章目录

  • 系列文章目录
  • 前言
  • 一、MyBatis兼容多数据的方式
    • 1、databaseIdProvider
    • 2、官方动态SQL方式
    • 3、MyBatis变量+动态SQL方式
  • 二、动态多数据源的问题
  • 总结


前言

一个系统要兼容多种数据库应该是很多系统都要面对的问题。曾经,Hibernate作为数据库层的王者,风光了十几年,说实在话,它在兼容多种数据库方面确实方便且功能强大,通过方言(dialect)就可以了。但是,目前笨重的Hibernate已经渐渐走出了历史舞台,MyBatis以轻巧性能高成为数据层的事实框架,而且扩展也非常之多。所有本章主要只会涉及到MyBatis中的相关知识。本章可参考源码。


提示:以下是本篇文章正文内容,下面案例可供参考

一、MyBatis兼容多数据的方式

1、databaseIdProvider

如果说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的结果:

  1. 如果没有配置databaseIdProvider的话,不会进入getDatabaseName方法,requiredDatabaseId此时为null,结果如下(只会匹配那些不包含databaseId的语句):
    兼容Oracle与MySQL的那些事_第1张图片
    兼容Oracle与MySQL的那些事_第2张图片

  2. 如果有配置databaseIdProvider,但是没有配置映射关系,那么requiredDatabaseId就是MySQL,此时databaseId必须完全匹配

@Bean
public DatabaseIdProvider databaseIdProvider() {
    return new VendorDatabaseIdProvider();
}

兼容Oracle与MySQL的那些事_第3张图片
兼容Oracle与MySQL的那些事_第4张图片

  1. 如果有配置databaseIdProvider而且有映射关系MySQL="mysql,那么requiredDatabaseId就是mysql,此时databaseId必须完全匹配,情况与上面相似,只是requiredDatabaseId不同而已,需要修改xml文件匹配。
  2. 如果有配置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>

兼容Oracle与MySQL的那些事_第5张图片

当然databaseId不仅仅是使用在select类型的statement当中,还可以在Insert, Update, Delete这些语句当中。比如下面这种方式(不同数据库的获取主键方式可能不同):

兼容Oracle与MySQL的那些事_第6张图片
对应的源码如下所示:

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);
}

2、官方动态SQL方式

如果配置了 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中配置值。
兼容Oracle与MySQL的那些事_第7张图片

3、MyBatis变量+动态SQL方式

另外通常在程序中大家可能按照如下模式来配置

<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文件中的变量都进行了替换。
兼容Oracle与MySQL的那些事_第8张图片
解析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);
}

兼容Oracle与MySQL的那些事_第9张图片

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
兼容Oracle与MySQL的那些事_第10张图片

注意不能写成如下格式(引用变量少了单引号)

"${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里面参数的修改会影响到所有的数据库请求,因为在切换数据库之前必须保证其他类型数据库的请求已经完成,否则会导致错误的查询。

你可能感兴趣的:(mysql,oracle,mybatis,mybatis)