Mybatis源碼之美:2.13.解析databaseIdProvider元素,配置數據庫類型唯一標誌生成器

解析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>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章