Mybatis源码研究之DatabaseIdProvider

借助Mybatis提供的 databaseId特性,我们可以实现让应用同时支持多种类型的数据库。

0. 目录这里写目录标题

1. 测试用例

相关的配置和测试用例如下 (这里我们以源生的mybatis为例,与SpringBoot的集成留待读者自行解读):

  1. mybatis-config.xml配置文件
    <environments default="development">
        <environment id="development">
            ....
        </environment>
    </environments>
 	
 	<!-- 启用databaseId支持 -->
    <databaseIdProvider type="DB_VENDOR">
    	<property name="Oracle" value="oracle"/>
    </databaseIdProvider>	    
    
    
    <mappers>
       ......
    </mappers>
  1. Mapper类
// Mapper
public interface MultiDbTestMapper {
	int getMtHisTableExists(@Param("dbName") String dbName, @Param("tableName") String tableName);
}
  1. Mapper文件
// mapper xml
<mapper namespace="xxx.mapper.MultiDbTestMapper">
    <!-- 查看某表是否存在 -->
    <select id="getMtHisTableExists" resultType="java.lang.Integer">
        select count(*) ICOUNT from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=#{dbName} and TABLE_NAME=upper(#{tableName})
    </select>
 
    <!-- oracle方言查看某表是否存在 -->
    <select id="getMtHisTableExists" resultType="java.lang.Integer" databaseId="oracle">
        SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME=upper(#{tableName})
    </select>
 
    <!-- sqlserver方言查看某表是否存在 -->
    <select id="getMtHisTableExists" resultType="java.lang.Integer" databaseId="sqlserver">
        select count(*) from dbo.sysobjects where id = object_id(upper(#{tableName})) and OBJECTPROPERTY(id, N'IsUserTable') = 1
    </select>

</mapper>
  1. 测试用例。
    @Autowired
    private MultiDbTestMapper mapper;
    
    @Test
    public void testName() throws Exception {
        Console.log(mapper.getMtHisTableExists("xxx_boot", "xxx_code"));
    }     
  1. 用例执行结果:
    1. mybatis将根据实际配置的数据库地址,从相同id的映射语句(<select>/<insert>/<update>/<delete>)中抓取相对应的进行执行。
    2. 以上操作由mybatis自行完成,使用者只需要正确配置数据库链接即可。无需其他额外操作。

2. 原理解析

先说结论:

  1. mybatis在启动阶段,根据数据源的配置,将xml映射文件中对应数据库类型的 xml节点语句加载到内存中,以供之后的调用。
  2. 以上述"getMtHisTableExists"为例,只会有其中一个被加载到内存中。

源码解读:

  1. 与当前环境下所使用的DataBase相适应的databaseId的获取。

    //================= XMLConfigBuilder.databaseIdProviderElement() , 对应于上述测试用例中mybatis-config.xml的配置
    //  1. 以下方法将为 Configuration 中的 databaseId 字段赋值。
    //  2. 赋值逻辑大致是通过用户注册的自定义DatabaseIdProvider实现类来完成
    //  3. DatabaseIdProvider默认实现为 VendorDatabaseIdProvider . 其中databaseId的获取逻辑为:根据实际的DataSource类型来获取配置的对应databaseId
      private void databaseIdProviderElement(XNode context) throws Exception {
        DatabaseIdProvider databaseIdProvider = null;
        if (context != null) {
          String type = context.getStringAttribute("type");
          // awful patch to keep backward compatibility
          if ("VENDOR".equals(type)) {
            // 在 Configuration 的构造函数中注册
            type = "DB_VENDOR";
          }
          Properties properties = context.getChildrenAsProperties();
          databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
          databaseIdProvider.setProperties(properties);
        }
        Environment environment = configuration.getEnvironment();
        if (environment != null && databaseIdProvider != null) {
          // 从实际使用的数据源中获取当前适用的databaseId
          String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
          configuration.setDatabaseId(databaseId);
        }
      }
    
  2. databaseId生效。

    //================= XMLStatementBuilder.parseStatementNode()
      public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
        // 如果当前 <select>/<update>/<insert>/<delete>节点上的 databaseId 属性值 与 全局配置上的 databaseId 不一致, 则直接跳过对其的解析, 不会将其加载到内存中
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
     
        String nodeName = context.getNode().getNodeName();
       	......
      }
     
     
      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;
      }
    

3. databaseId的其它应用

  1. databaseId的另外一种使用方式: @Select注解。

    
    // 注解的方式实现SQL, 效果等同于上述测试用例中的映射文件
    // 底层支撑类: MapperAnnotationBuilder.AnnotationWrapper
    @Select(value = "select count(*) ICOUNT from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=#{dbName} and TABLE_NAME=upper(#{tableName})", databaseId = "mysql")
    @Select(value = "SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME=upper(#{tableName})", databaseId = "oracle")
    int getMtHisTableExistsByAnnotation(@Param("dbName") String dbName, @Param("tableName") String tableName);
    
  2. 通过占位符,在映射语句中获取当前的databaseId。(支撑类为DynamicContext

    <select id="getMtHisTableExists" resultType="java.lang.Integer" databaseId="oracle">
        SELECT '${_databaseId}' as databaseId FROM USER_TABLES WHERE TABLE_NAME=upper(#{tableName})      
    </select>
    

4. Links

  1. spring boot项目中mybatis plus多数据库支持
  2. MyBatis之databaseIdProvider多数据库支持
上一篇:你有一份经典SQL语句大全,请注意查收!!!


下一篇:Hbase操作与编程使用