Mybatis源码之美:2.13.解析databaseIdProvider元素,配置数据库类型唯一标志生成器

原创
2020/06/27 10:28
阅读数 391

解析databaseIdProvider元素,配置数据库类型唯一标志生成器

mybatis中定义了一个名为DatabaseIdProvider的接口,该接口的作用是获取不同数据源在mybatis中的唯一标志。

DatabaseIdProvider定义了两个方法,setProperties()方法用于配置自定义属性,getDatabaseId()方法用于获取指定数据源对应的databaseId

/**
 * 在需要使用多数据库特性的时候,可以实现该接口来构建自己的DatabaseIdProvider
 * <p>
 * @author Eduardo Macarron
 */
public interface DatabaseIdProvider {

    // 配置自定义属性
    void setProperties(Properties p);

    /**
     * 获取指定数据源的databaseId
     *
     * @param dataSource 数据源
     */
    String getDatabaseId(DataSource dataSource) throws SQLException;
}

通常来说,setProperties()方法会在getDatabaseId()方法前被调用。

借助于DatabaseIdProvider和映射语句中配置的databaseId属性,mybatis可以在运行时根据数据源的不同来执行不同的SQL语句。

mybatis对生效语句的筛选逻辑是:

MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。 如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。

记着这个筛选逻辑,我们通过一个单元测试来深入了解DatabaseIdProvider

mybatis的单元测试包中包含一个名为MultiDbTest的测试类,该类位于org.apache.ibatis.submitted.multidb包下。

> org.apache.ibatis.submitted.multidb包下的类和文件,主要用于测试关于多数据源的功能。

MultiDbTest中定义了一个setUp()方法,这个方法会在单元测试运行前执行,主要负责初始化SqlSessionFactory对象和初始化数据库信息。

  protected static SqlSessionFactory sqlSessionFactory;

  @BeforeAll
  public static void setUp() throws Exception {
    try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/multidb/MultiDbConfig.xml")) {
      // 初始化sqlSessionFactory
      sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    }
    // 初始化数据库
    BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(),
            "org/apache/ibatis/submitted/multidb/CreateDB.sql");
  }

用于初始化SqlSessionFactory对象的MultiDbConfig.xml配置文件比较简单:

<configuration>

    <!-- 配置数据源环境-->
    <environments default="development">
        <environment id="development">
            <transactionmanager type="JDBC">
                <property name="" value="" />
            </transactionmanager>
            <datasource type="UNPOOLED">
                <property name="driver" value="org.hsqldb.jdbcDriver" />
                <property name="url" value="jdbc:hsqldb:mem:multidb" />
                <property name="username" value="sa" />
            </datasource>
        </environment>
    </environments>

    <!-- 配置DatabaseIdProvider实例-->
    <databaseidprovider type="DB_VENDOR">
        <property name="HSQL Database Engine" value="hsql" />
    </databaseidprovider>

    <!-- 引入mappers-->
    <mappers>
        <mapper resource="org/apache/ibatis/submitted/multidb/MultiDbMapper.xml" />
    </mappers>

</configuration>

他配置了一个名为development的数据源环境,指定了用于获取databaseIdDatabaseIdProvider实例,并引入了MultiDbMapper对象对应的mapper文件——MultiDbMapper.xml

development数据源对应的初始化脚本为CreateDB.sql,脚本中创建了commonhsql两个表,并往两张表中各插入了一条id1的同名数据。

create table common (
id int,
name varchar(20)
);

create table hsql (
id int,
name varchar(20)
);

insert into common (id, name) values(1, 'common');

insert into hsql (id, name) values(1, 'hsql');

我们这次要看的是MultiDbTest中名为shouldExecuteHsqlQuery()的单元测试方法:

@Test
public void shouldExecuteHsqlQuery() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      // 获取MultiDbMapper的代理对象
      MultiDbMapper mapper = sqlSession.getMapper(MultiDbMapper.class);
      // 执行简单查询
      String answer = mapper.select1(1);
      assertEquals("hsql", answer);
    }
}

这个方法比较简单,调用了MultiDbMapperselect1()方法进行了一次简单的查询操作。

MultiDbMapper.xml文件中关于select1方法的定义有两个:

<mapper namespace="org.apache.ibatis.submitted.multidb.MultiDbMapper">
    
    <!-- 未指定databaseId,从common表中查询数据 -->
    <select id="select1" resulttype="string" parametertype="int">
    select
    name from common where id=#{value}
    </select>
    
    <!-- 指定了databaseId的值为hsql,从hsql表中查询数据-->
    <select id="select1" databaseid="hsql" resulttype="string" parametertype="int">
    select name from hsql where
    id=#{value}
    </select>
</mapper>	

在单元测试中,我们可以看到MultiDbMapperselect1()方法的返回值是hsql,也就意味着配置了databaseId="hsql"的声明语句生效了。

根据前面我们了解的mybatis筛选有效声明语句的逻辑,可以推断出,当前数据源环境对应的databaseIdhsql

这个hsql就是使用别名为DB_VENDORDatabaseIdProvider实例获取的。

MybaitsConfiguration的构造方法中注册了别名DB_VENDOR,该别名指向了VendorDatabaseIdProvider类型。

public Configuration() {

  ...
  // 注册处理数据库ID的提供者
    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
  ...
}

VendorDatabaseIdProvidermybatis中惟一一个默认注册的DatabaseIdProvider实例,该实例基于数据库链接元数据DatabaseMetaData对象的getDatabaseProductName()方法来获取指定数据源对应的数据库产品名称

通常来说数据库产品名称是一个很长的字符串,而且同一数据库的不同版本得到的数据库产品名称可能也不一致,因此,为了提供更好的用户体验,增强代码兼容性和减少代码量,

VendorDatabaseIdProvider允许用户提供一个包含数据库产品名称databaseId对应关系的Properties对象,并由此推断出合适的databaseId

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;
    }
    // 如果用户不传properties,会直接使用 数据库产品名称 作为当前databaseId
    return productName;
}

在当前单元测试中,指定了数据库产品名称中包含字符串HSQL Database Engine的数据源对应的databaseIdhsql

<!-- 配置DatabaseIdProvider实例-->
<databaseidprovider type="DB_VENDOR">
    <property name="HSQL Database Engine" value="hsql" />
</databaseidprovider>

我们当前使用的是数据库驱动是org.hsqldb.jdbcDriver,因此对应着DatabaseMetaData的实例就是org.hsqldb.jdbc.JDBCDatabaseMetaData

该实例的getDatabaseProductName()方法定义如下:

public String getDatabaseProductName() throws SQLException {
    return "HSQL Database Engine";
}

方法返回值刚好和property元素中定义的name属性相匹配,因此该property元素的valuehsql就是最终得到的databaseId

在了解了DatabaseIdProvider的定义和实现之后,我们来看一下databaseIdProvider元素的DTD定义:

<!--ELEMENT databaseIdProvider (property*)-->
<!--ATTLIST databaseIdProvider
type CDATA #REQUIRED
-->

databaseIdProvider有一个必填的属性type,指定了DatabaseIdProvider的实现类,该参数可以使用Mybatis中配置的别名。

databaseIdProvider还允许定义零个或多个property子元素,这些子元素最终会被转换为Properties对象,借由DatabaseIdProvidersetProperties()方法传递给DatabaseIdProvider的实现类完成进一步的处理。

比如:VendorDatabaseIdProvider中用户指定数据库产品名称databaseId对应关系,就是通过databaseIdProvider元素的property子元素来实现的:

  <databaseidprovider type="DB_VENDOR">
    <property name="Apache Derby" value="derby" />
    <property name="SQL Server" value="sqlserver" />
    <property name="DB2" value="db2" />        
    <property name="Oracle" value="oracle" />
  </databaseidprovider>

databaseIdProvider元素的解析代码也比较简单:

/**
 * 解析 databaseIdProvider节点
 *
 * @param context databaseIdProvider节点
 */
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)) {
            type = "DB_VENDOR";
        }
        // 获取用户定义的数据库类型和databaseId的配置
        Properties properties = context.getChildrenAsProperties();
        // 获取databaseIdProvider实例
        databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
        // 配置数据库类型和databaseId的对应关系
        databaseIdProvider.setProperties(properties);
    }
    // 获取Environment容器
    Environment environment = configuration.getEnvironment();
    if (environment != null &amp;&amp; databaseIdProvider != null) {
        // 获取当前环境的databaseId
        String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
        // 同步configuration#databaseId的值
        configuration.setDatabaseId(databaseId);
    }
}

首先解析所有的property子元素获取到对应的Properties对象,然后解析出databaseIdProvidertype属性对应的DatabaseIdProvider实例类型,之后通过反射操作获取对应的DatabaseIdProvider实例。

拿到DatabaseIdProvider实例之后,调用该实例的setProperties()方法完成Properties对象的后续处理操作。

到这里DatabaseIdProvider实例的初始化工作就完成了。

之后获取Mybatis当前生效的Environment对象(Configuration#environment)中的数据源,解析出该数据源对应的databaseId,并将其赋值给ConfigurationdatabaseId属性。

到这为止,databaseIdProvider元素的解析也已经完成。

关注我,一起学习更多知识

关注我</object,></p>

展开阅读全文
打赏
0
0 收藏
分享
加载中
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部