深入分析 iBATIS 框架之系統架構與映射原理

深入分析 iBATIS 框架之系統架構與映射原理

簡介: iBATIS 通過 SQL Map 將 Java 對象映射成 SQL 語句和將結果集再轉化成 Java 對象,與其他 ORM 框架相比,既解決了 Java 對象與輸入參數和結果集的映射,又能夠讓用戶方便的手寫使用 SQL 語句。本文主要介紹了 iBATIS 框架的體系結構和運行流程,以及 iBATIS 如何完成 SQL 語句的解析與 Java 對象與數據字段映射關係的建立,最後用一個實例說明了 iBATIS 是如何幫我們完成工作的。


iBATIS 框架主要的類層次結構

總體來說 iBATIS 的系統結構還是比較簡單的,它主要完成兩件事情:

  1. 根據 JDBC 規範建立與數據庫的連接;

  2. 通過反射打通 Java 對象與數據庫參數交互之間相互轉化關係。

iBATIS 的框架結構也是按照這種思想來組織類層次結構的,其實它是一種典型的交互式框架。先期準備好交互的必要條件,然後構建一個交互的環境,交互環境中還劃分成會話,每次的會話也有一個環境。當這些環境都準備好了以後,剩下的就是交換數據了。其實涉及到網絡通信,一般都會是類似的處理方式。

圖 1 是 iBATIS 框架的主要的類層次結構圖:


圖 1. iBATIS 框架的主要的類層次結構圖
图 1. iBATIS 框架的主要的类层次结构图

上面的類圖中左邊 SqlMapClient 接口主要定義了客戶端的操作行爲包括 select、insert、update、delete。而右邊主要是定義了當前客戶端在當前線程的執行環境。SqlMapSession 可以共享使用,也可以自己創建,如果是自己創建在結束時必須要調用關閉接口關閉。

當使用者持有了 SqlMapClientImpl 對象就可以使用 iBATIS 來工作了。這裏還要提到另外一個類 SqlMapExecutorDelegate 這個類從名字就可以看出他是執行代理類。這個類非常重要,重要是因爲他耦合了用戶端的執行操作行爲和執行的環境,他持有執行操作的所需要的數據,同時提供管理着執行操作依賴的環境。所以他是一個強耦合的類,也可以看做是個工具類。

iBATIS 框架的設計策略

iBATIS 主要的設計目的還是爲了讓我們執行 SQL 時對輸入輸出的數據管理更加方便,所以如何方便的讓我們寫出 SQL 和方便的獲取 SQL 的執行結果纔是 iBATIS 的核心競爭力。那麼 iBATIS 是怎麼實現它的核心競爭力的呢?

iBATIS 框架的一個重要組成部分就是其 SqlMap 配置文件,SqlMap 配置文件的核心是 Statement 語句包括 CIUD。 iBATIS 通過解析 SqlMap 配置文件得到所有的 Statement 執行語句,同時會形成 ParameterMap、ResultMap 兩個對象用於處理參數和經過解析後交給數據庫處理的 Sql 對象。這樣除去數據庫的連接,一條 SQL 的執行條件已經具備了。

圖 2 描述了 Statement 有關的類結構圖:


圖 2. Statement 有關的類結構圖
图 2. Statement 有关的类结构图

圖 2 給出了圍繞 SQL 執行的基本的結構關係,但是還有一個關鍵的部分就是,如何定義 SQL 語句中的參數與 Java 對象之間的關係,這其中還涉及到 Java 類型到數據庫類型的轉換等一系列問題。

數據的映射大體的過程是這樣的:根據 Statement 中定義的 SQL 語句,解析出其中的參數,按照其出現的順序保存在 Map 集合中,並按照 Statement 中定義的 ParameterMap 對象類型解析出參數的 Java 數據類型。並根據其數據類型構建 TypeHandler 對象,參數值的複製是通過 DataExchange 對象完成的。

圖 3 是參數映射相關的類結構圖:


圖 3. 參數映射相關的類結構圖
图 3. 参数映射相关的类结构图

圖 3 是輸入參數的映射結構情況,返回結果 ResultMap 的映射情況也是類似的。主要就是要解決 SQL 語句中的參數與返回結果的列名與 Statement 中定義的 parameterClass 和 resultClass 中屬性的對應關係。

iBATIS 框架的運行原理

前面大體分析了 iBATIS 框架的主要類的結構,這裏主要看一下這些類是如何串聯起來、如何工作的。圖 4 描述了整個過程的主要執行步驟。


圖 4.iBATIS 運行的主要執行步驟
图 4.iBATIS 运行的主要执行步骤

上圖中描述的 SqlMapSession 對象的創建和釋放根據不同情況會有不同,因爲 SqlMapSession 負責創建數據庫的連接,包括對事務的管理,iBATIS 對管理事務既可以自己管理也可以由外部管理,iBATIS 自己管理是通過共享 SqlMapSession 對象實現的,多個 Statement 的執行時共享一個 SqlMapSession 實例,而且都是線程安全的。如果是外部程序管理就要自己控制 SqlMapSession 對象的生命週期。

圖 5 是通過 Spring 調用 iBATIS 執行一個 Statement 的一個詳細的時序圖:


圖 5. Spring 調用 iBATIS 執行一個 Statement 的時序圖
图 5. Spring 调用 iBATIS 执行一个 Statement 的时序图

(查看圖 5 的 清晰版本。)

iBATIS 的主要工作連接、交互,所以必須根據不同的交易成本設計不同的交易環境。

示例

下面我們將根據一個具體的實例解析一個 Statement 如何完成映射的,我們用一個典型的查詢語句看看 Java 對象中的數據時如何賦給 SQL 中的參數的,再看看 SQL 的查詢結果是如何轉成 Java 對象的。

先看一下示例的部分代碼和配置文件,完整的代碼請看附件。

Spring 的 applicationContext 配置文件:


清單 1. applicationContext.xml
				
<beans>
    <bean id="sqlMapTransactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <bean id="sqlMapTransactionTemplate" 
        class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="sqlMapTransactionManager"/>
    </bean>
    <!--sql map -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="configLocation" value="com/mydomain/data/SqlMapConfig.xml"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <bean id="dataSource" name="dataSource" 
        class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@10.1.5.11:1521:XE"/>
        <property name="username" value="junshan"/>
        <property name="password" value="junshan"/>
        <property name="maxActive" value="20"/>
    </bean>
    <bean id="accountDAO" class="com.mydomain.AccountDAO">
        <property name="sqlMapClient" ref="sqlMapClient"/>
        <property name="sqlMapTransactionTemplate" ref="sqlMapTransactionTemplate"/>
    </bean>
</beans>

下面是 Account.xml 的一個 Statement:


清單 2. Account.xml 中一個 Statement
				
<select id="selectAccount" parameterClass="Account" resultClass="Account">
    select
      ACC_ID,
      ACC_FIRST_NAME as firstName,
      ACC_LAST_NAME as lastName,
      ACC_EMAIL as emailAddress,
      ACC_DATE
    from ACCOUNT
    where ACC_ID = #id:INTEGER# and ACC_FIRST_NAME = #firstName#
</select>

下面是 Java 的測試類:


清單 3. SimpleTest
				
public class SimpleTest {
    public static void main(String[] args) {
        ApplicationContext factory = 
        new ClassPathXmlApplicationContext("/com/mydomain/data/applicationContext.xml");
        final AccountDAO accountDAO = (AccountDAO) factory.getBean("accountDAO");
        final Account account = new Account();
        account.setId(1);
        account.setFirstName("tao");
        account.setLastName("bao");
        account.setEmailAddress("[email protected]");
        account.setDate(new Date());
        try {
            accountDAO.getSqlMapTransactionTemplate().execute(new TransactionCallback(){
                    public Object doInTransaction(TransactionStatus status){
                        try{
                            accountDAO.deleteAccount(account.getId());
                            accountDAO.insertAccount(account);
                            //account.setLastName("bobo");
                            //accountDAO.updateAccount(account);
                            Account result = accountDAO.selectAccount(account);
                            System.out.println(result);
                            return null;
                        } catch (Exception e) {
                            status.setRollbackOnly();
                            return false;
                        }
                    }
                });
            //accountDAO.getSqlMapClient().commitTransaction();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
}

iBATIS 對 SQL 語句的解析

這裏所說的 SQL 解析只是針對 iBATIS 配置文件中所定義的 SQL 語句,如前一節中清單 2 中所示的查詢語句。和標準的 SQL 語句不同的是,參數的賦值是“#“包裹的變量名。如何解析這個變量就是 iBATIS 要完成的工作。當然 SQL 的表達形式還有很多其他的形式如動態 SQL 等。

現在我們關心的是當我們執行:


清單 4. 執行查詢方法
				
accountDAO.selectAccountById(account) 

iBATIS 將會選擇清單 2 這條 Statement 來解析,最終會把它解析成一個標準的 SQL 提交給數據庫執行,並且會設置兩個選擇條件參數。這個過程中參數映射的細節是什麼樣子呢?

在前面的第二小節中已經說明了,iBATIS 會把 SqlMap 配置文件解析成一個個 Statement,其中包括 ParameterMap、ResultMap,以及解析後的 SQL。當 iBATIS 構建好 RequestScope 執行環境後,要做的工作就是把傳過來的對象數據結合 ParameterMap 中信息提取出一個參數數組,這個數組的順序就是對應於 SQL 中參數的順序,然後會調用 preparedStatement.setXXX(i, parameter) 提交參數。

在清單 3 中,我們給 account 對象的 id 屬性和 firstName 屬性分別賦值爲 1 和“tao“,當執行清單 4 中的這段代碼時,iBATIS 必須把這兩個屬性值傳給清單 2 中 SQL 語句中對象的參數。這個是怎麼做到的,其實很簡單,在圖 3 中描述了與 ParameterMap 相關的類的關係,這些類中都保存了在 SqlMap 配置文件初始化是解析清單 2 中 Statement 的所有必要的信息,具體的信息是這樣的:

最終的 SQL 語句是:


清單 5. 解析後的 SQL
				
select
    ACC_ID,
    ACC_FIRST_NAME as firstName,
    ACC_LAST_NAME as lastName,
    ACC_EMAIL as emailAddress,
    ACC_DATE
from ACCOUNT
where ACC_ID = ? and ACC_FIRST_NAME = ? 

#id:INTEGER# 將被解析成 JDBC 類型是 INTEGER,參數值取 Account 對象的 id 屬性。#firstName# 同樣被解析成 Account 對象的 firstName 屬性,而 parameterClass="Account"指明瞭 Account 的類類型。注意到清單 5 中 #id:INTEGER# 和 #firstName# 都被替換成“?”,iBATIS 如何保證它們的順序?在解析清單 2 過程中,iBATIS 會根據“#”分隔符取出合法的變量名構建參數對象數組,數組的順序就是 SQL 中變量出現的順序。接着 iBATIS 會根據這些變量和 parameterClass 指定的類型創建合適的 dataExchange 和 parameterPlan 對象。parameterPlan 對象中按照前面的順序保存了變量的 setter 和 getter 方法列表。

所以 parameter 的賦值就是根據 parameterPlan 中保存的 getter 方法列表以及傳進來的 account 對象利用反射機制得到清單 5 對應的參數值數組,再將這個數組按照指定的 JDBC 類型提交給數據庫。以上這些過程可以用圖 6 的時序圖清楚的描述:


圖 6. 映射參數值到數據庫過程時序圖
图 6. 映射参数值到数据库过程时序图

上圖 4 中在 8 步驟中如果 value 值爲空時會設置 preparedStatement.setNull(i , jdbcType) 如果在清單 2 中的變量沒有設置 jdbcType 類型時有可能會出錯。

數據庫字段映射到 Java 對象

數據庫執行完 SQL 後會返回執行結果,在第 4 小節的例子中滿足 id 爲 1、firstName 爲“tao”的信息有兩條,iBATIS 如何將這兩條記錄設置到 account 對象中呢?

和 ParameterMap 類似,填充返回信息需要的資源都已經包含在 ResultMap 中。當有了保存返回結果的 ResultSet 對象後,就是要把列名映射到 account 對象的對應屬性中。這個過程大體如下:

根據 ResultMap 中定義的 ResultClass 創建返回對象,這裏就是 account 對象。獲取這個對象的所有可寫的也就是 setter 方法的屬性數組,接着根據返回 ResultSet 中的列名去匹配前面的屬性數組,把匹配結果構造成一個集合(resultMappingList),後面是選擇 DataExchange 類型、AccessPlan 類型爲後面的真正的數據交換提供支持。根據 resultMappingList 集合從 ResultSet 中取出列對應的值,構成值數組(columnValues),這個數組的順序就是 SQL 中對應列名的順序。最後把 columnValues 值調用 account 對象的屬性的 setter 方法設置到對象中。這個過程可以用下面的時序圖來表示:


圖 7. 映射返回對象時序圖
图 7. 映射返回对象时序图

示例運行的結果

前兩個小節主要描述了輸入參數和輸出結果的映射原理,這裏再結合第 4 小節的示例分析一下執行清單 3 代碼的結果。

執行清單 3 所示代碼打印的結果爲:


清單 6. 示例程序的運行結果
				
Account{id=0, firstName='tao', lastName='bobo', emailAddress='[email protected]'}

上面的結果和我們預想的結果似乎有所不同,看代碼我們插入數據庫的 account 對象各屬性值分別爲 {1,“tao”,“bao”,“[email protected]”,“時間”},後面調用清單 2 的查詢,返回應該是一樣的結果纔對。id 的結果不對、date 屬性值丟失。再仔細看看清單 2 這個 Statement 可以發現,返回結果的列名分別是 {ACC_ID,firstName,lastName,emailAddress,ACC_DATE} 其中 id 和 date 並不能映射到 Account 類的屬性中。id 被賦了默認數字 0,而 date 沒有被賦值。

還有一個值得注意的地方是變量 id 後面跟上 JDBC 類型,這個 JDBC 類型有沒有用?通常情況下都沒有用,因此你可以不設,iBATIS 會自動選擇默認的類型。但是如果你要這個這個值可能爲空時如果沒有指定 JDBC 類型可能就有問題了,在 Oracle 中雖然能正常工作但是會引起 Oracle 這當前這個 SQL 有多次編譯現象,因此會影響數據庫的性能。還有當同一個 Java 類型如果對應多個 JDBC 類型(如 Date 對應 JDBC 類型有 java.sql.Date、java.sql.Timestamp)就可以通過指定 JDBC 類型保存不同的值到數據庫。

總結

如果用最簡潔的話來總結 iBATIS 主要完成那些功能時,我想下面幾個代碼足夠概括。


清單 7. 典型的 Java 操作數據庫代碼
				
Class.forName("oracle.jdbc.driver.OracleDriver"); 
Connection conn= DriverManager.getConnection(url,user,password);
java.sql.PreparedStatement  st = conn.prepareStatement(sql);
st.setInt(0,1);
st.execute();
java.sql.ResultSet rs =  st.getResultSet();
while(rs.next()){
    String result = rs.getString(colname);
}

iBATIS 就是將上面這幾行代碼分解包裝,但是最終執行的仍然是這幾行代碼。前兩行是對數據庫的數據源的管理包括事務管理,3、4 兩行 iBATIS 通過配置文件來管理 SQL 以及輸入參數的映射,6、7、8 行是 iBATIS 獲取返回結果到 Java 對象的映射,他也是通過配置文件管理。

配置文件對應到相應代碼如圖所示:


圖 8. 配置文件與相應代碼對應關係
图 8. 配置文件与相应代码对应关系

iBATIS 要達到目的就是把用戶關心的和容易變化的數據放到配置文件中配置,方便用戶管理。而把流程性的、固定不變的交給 iBATIS 來實現。這樣是用戶操作數據庫簡單、方便,這也是 iBATIS 的價值所在。


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