MyBatis 架構與原理深入解析
1 引言#
本文主要講解 JDBC 怎麼演變到 Mybatis 的漸變過程,重點講解了爲什麼要將 JDBC 封裝成 Mybaits 這樣一個持久層框架。再而論述 Mybatis 作爲一個數據持久層框架本身有待改進之處。
2 JDBC 實現查詢分析#
我們先看看我們最熟悉也是最基礎的通過 JDBC 查詢數據庫數據,一般需要以下七個步驟:
加載 JDBC 驅動;
建立並獲取數據庫連接;
創建 JDBC Statements 對象;
設置 SQL 語句的傳入參數;
執行 SQL 語句並獲得查詢結果;
對查詢結果進行轉換處理並將處理結果返回;
釋放相關資源(關閉 Connection,關閉 Statement,關閉 ResultSet);
以下是具體的實現代碼:
public static List<Map<String,Object>> queryForList(){
Connection connection = null;
ResultSet rs = null;
PreparedStatement stmt = null;
List<Map<String,Object>> resultList = new ArrayList<Map<String,Object>>();
try {
// 加載JDBC驅動
Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
String url = "jdbc:oracle:thin:@localhost:1521:ORACLEDB";
String user = "trainer";
String password = "trainer";
// 獲取數據庫連接
connection = DriverManager.getConnection(url,user,password);
String sql = "select * from userinfo where user_id = ? ";
// 創建Statement對象(每一個Statement爲一次數據庫執行請求)
stmt = connection.prepareStatement(sql);
// 設置傳入參數
stmt.setString(1, "zhangsan");
// 執行SQL語句
rs = stmt.executeQuery();
// 處理查詢結果(將查詢結果轉換成List<Map>格式)
ResultSetMetaData rsmd = rs.getMetaData();
int num = rsmd.getColumnCount();
while(rs.next()){
Map map = new HashMap();
for(int i = 0;i < num;i++){
String columnName = rsmd.getColumnName(i+1);
map.put(columnName,rs.getString(columnName));
}
resultList.add(map);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 關閉結果集
if (rs != null) {
rs.close();
rs = null;
}
// 關閉執行
if (stmt != null) {
stmt.close();
stmt = null;
}
if (connection != null) {
connection.close();
connection = null;
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return resultList;
}
3 JDBC 演變到 Mybatis 過程#
上面我們看到了實現 JDBC 有七個步驟,哪些步驟是可以進一步封裝的,減少我們開發的代碼量。
3.1 第一步優化:連接獲取和釋放 ##
- 問題描述:
數據庫連接頻繁的開啓和關閉本身就造成了資源的浪費,影響系統的性能。
解決問題:
數據庫連接的獲取和關閉我們可以使用數據庫連接池來解決資源浪費的問題。通過連接池就可以反覆利用已經建立的連接去訪問數據庫了。減少連接的開啓和關閉的時間。
- 問題描述:
但是現在連接池多種多樣,可能存在變化,有可能採用 DBCP 的連接池,也有可能採用容器本身的 JNDI 數據庫連接池。
解決問題:
我們可以通過 DataSource 進行隔離解耦,我們統一從 DataSource 裏面獲取數據庫連接,DataSource 具體由 DBCP 實現還是由容器的 JNDI 實現都可以,所以我們將 DataSource 的具體實現通過讓用戶配置來應對變化。
3.2 第二步優化:SQL 統一存取 ##
- 問題描述:
我們使用 JDBC 進行操作數據庫時,SQL 語句基本都散落在各個 JAVA 類中,這樣有三個不足之處:
第一,可讀性很差,不利於維護以及做性能調優。
第二,改動 Java 代碼需要重新編譯、打包部署。
第三,不利於取出 SQL 在數據庫客戶端執行(取出後還得刪掉中間的 Java 代碼,編寫好的 SQL 語句寫好後還得通過+號在 Java 進行拼湊)。
解決問題:
我們可以考慮不把 SQL 語句寫到 Java 代碼中,那麼把 SQL 語句放到哪裏呢?首先需要有一個統一存放的地方,我們可以將這些 SQL 語句統一集中放到配置文件或者數據庫裏面(以 key-value 的格式存放)。然後通過 SQL 語句的 key 值去獲取對應的 SQL 語句。
既然我們將 SQL 語句都統一放在配置文件或者數據庫中,那麼這裏就涉及一個 SQL 語句的加載問題。
3.3 第三步優化:傳入參數映射和動態 SQL##
- 問題描述:
很多情況下,我們都可以通過在 SQL 語句中設置佔位符來達到使用傳入參數的目的,這種方式本身就有一定侷限性,它是按照一定順序傳入參數的,要與佔位符一一匹配。但是,如果我們傳入的參數是不確定的(比如列表查詢,根據用戶填寫的查詢條件不同,傳入查詢的參數也是不同的,有時是一個參數、有時可能是三個參數),那麼我們就得在後臺代碼中自己根據請求的傳入參數去拼湊相應的 SQL 語句,這樣的話還是避免不了在 Java 代碼裏面寫 SQL 語句的命運。既然我們已經把 SQL 語句統一存放在配置文件或者數據庫中了,怎麼做到能夠根據前臺傳入參數的不同,動態生成對應的 SQL 語句呢?
解決問題:
第一,我們先解決這個動態問題,按照我們正常的程序員思維是,通過 if 和 else 這類的判斷來進行是最直觀的,這個時候我們想到了 JSTL 中的 這樣的標籤,那麼,能不能將這類的標籤引入到 SQL 語句中呢?假設可以,那麼我們這裏就需要一個專門的 SQL 解析器來解析這樣的 SQL 語句,但是,if 判斷的變量來自於哪裏呢?傳入的值本身是可變的,那麼我們得爲這個值定義一個不變的變量名稱,而且這個變量名稱必須和對應的值要有對應關係,可以通過這個變量名稱找到對應的值,這個時候我們想到了 key-value 的 Map。解析的時候根據變量名的具體值來判斷。
假如前面可以判斷沒有問題,那麼假如判斷的結果是 true,那麼就需要輸出的標籤裏面的 SQL 片段,但是怎麼解決在標籤裏面使用變量名稱的問題呢?這裏我們需要使用一種有別於 SQL 的語法來嵌入變量(比如使用#變量名#)。這樣,SQL 語句經過解析後就可以動態的生成符合上下文的 SQL 語句。
還有,**怎麼區分開佔位符變量和非佔位變量?**有時候我們單單使用佔位符是滿足不了的,佔位符只能爲查詢條件佔位,SQL 語句其他地方使用不了。這裏我們可以使用 #變量名# 表示佔位符變量,使用變量名錶示非佔位符變量。
3.4 第四步優化:結果映射和結果緩存 ##
- 問題描述:
執行 SQL 語句、獲取執行結果、對執行結果進行轉換處理、釋放相關資源是一整套下來的。假如是執行查詢語句,那麼執行 SQL 語句後,返回的是一個 ResultSet 結果集,這個時候我們就需要將 ResultSet 對象的數據取出來,不然等到釋放資源時就取不到這些結果信息了。我們從前面的優化來看,以及將獲取連接、設置傳入參數、執行 SQL 語句、釋放資源這些都封裝起來了,只剩下結果處理這塊還沒有進行封裝,如果能封裝起來,每個數據庫操作都不用自己寫那麼一大堆 Java 代碼,直接調用一個封裝的方法就可以搞定了。
解決問題:
我們分析一下,一般對執行結果的有哪些處理,有可能將結果不做任何處理就直接返回,也有可能將結果轉換成一個 JavaBean 對象返回、一個 Map 返回、一個 List 返回等 `,結果處理可能是多種多樣的。從這裏看,我們必須告訴 SQL 處理器兩點:第一,需要返回什麼類型的對象;第二,需要返回的對象的數據結構怎麼跟執行的結果映射,這樣才能將具體的值 copy 到對應的數據結構上。
接下來,我們可以進而考慮對 SQL 執行結果的緩存來提升性能。緩存數據都是 key-value 的格式,那麼這個 key 怎麼來呢?怎麼保證唯一呢?即使同一條 SQL 語句幾次訪問的過程中由於傳入參數的不同,得到的執行 SQL 語句也是不同的。那麼緩存起來的時候是多對。但是 SQL 語句和傳入參數兩部分合起來可以作爲數據緩存的 key 值。
3.5 第五步優化:解決重複 SQL 語句問題 ##
- 問題描述:
由於我們將所有 SQL 語句都放到配置文件中,這個時候會遇到一個 SQL 重複的問題,幾個功能的 SQL 語句其實都差不多,有些可能是 SELECT 後面那段不同、有些可能是 WHERE 語句不同。有時候表結構改了,那麼我們就需要改多個地方,不利於維護。
解決問題:
當我們的代碼程序出現重複代碼時怎麼辦?將重複的代碼抽離出來成爲獨立的一個類,然後在各個需要使用的地方進行引用。對於 SQL 重複的問題,我們也可以採用這種方式,通過將 SQL 片段模塊化,將重複的 SQL 片段獨立成一個 SQL 塊,然後在各個 SQL 語句引用重複的 SQL 塊,這樣需要修改時只需要修改一處即可。
4 Mybaits 有待改進之處#
- 問題描述:
Mybaits 所有的數據庫操作都是基於 SQL 語句,導致什麼樣的數據庫操作都要寫 SQL 語句。一個應用系統要寫的 SQL 語句實在太多了。
改進方法:
我們對數據庫進行的操作大部分都是對錶數據的增刪改查,很多都是對單表的數據進行操作,由這點我們可以想到一個問題:單表操作可不可以不寫 SQL 語句,通過 JavaBean 的默認映射器生成對應的 SQL 語句,比如:一個類 UserInfo 對應於 USER_INFO 表, userId 屬性對應於 USER_ID 字段。這樣我們就可以通過反射可以獲取到對應的表結構了,拼湊成對應的 SQL 語句顯然不是問題。
5 MyBatis 框架整體設計#
5.1 接口層 - 和數據庫交互的方式#
MyBatis 和數據庫的交互有兩種方式:
使用傳統的 MyBatis 提供的 API;
使用 Mapper 接口;
5.1.1 使用傳統的 MyBatis 提供的 API###
這是傳統的傳遞 Statement Id 和查詢參數給 SqlSession 對象,使用 SqlSession 對象完成和數據庫的交互;MyBatis 提供了非常方便和簡單的 API,供用戶實現對數據庫的增刪改查數據操作,以及對數據庫連接信息和 MyBatis 自身配置信息的維護操作。
上述使用 MyBatis 的方法,是創建一個和數據庫打交道的 SqlSession 對象,然後根據 Statement Id 和參數來操作數據庫,這種方式固然很簡單和實用,但是它不符合面嚮對象語言的概念和麪向接口編程的編程習慣。由於面向接口的編程是面向對象的大趨勢,MyBatis 爲了適應這一趨勢,增加了第二種使用 MyBatis 支持接口(Interface)調用方式。
5.1.2 使用 Mapper 接口
MyBatis 將配置文件中的每一個 節點抽象爲一個 Mapper 接口:
這個接口中聲明的方法和 節點中的 < select|update|delete|insert> 節點項對應,即 <select|update|delete|insert> 節點的 id 值爲 Mapper 接口中的方法名稱,parameterType 值表示 Mapper 對應方法的入參類型,而 resultMap 值則對應了 Mapper 接口表示的返回值類型或者返回結果集的元素類型。
根據 MyBatis 的配置規範配置好後,通過 SqlSession.getMapper(XXXMapper.class) 方法,MyBatis 會根據相應的接口聲明的方法信息,通過動態代理機制生成一個 Mapper 實例,我們使用 Mapper 接口的某一個方法時,MyBatis 會根據這個方法的方法名和參數類型,確定 Statement Id,底層還是通過 SqlSession.select("statementId",parameterObject); 或者 SqlSession.update("statementId",parameterObject); 等等來實現對數據庫的操作,MyBatis 引用 Mapper 接口這種調用方式,純粹是爲了滿足面向接口編程的需要。(其實還有一個原因是在於,面向接口的編程,使得用戶在接口上可以使用註解來配置 SQL 語句,這樣就可以脫離 XML 配置文件,實現 “0 配置”)。
5.2 數據處理層 ##
數據處理層可以說是 MyBatis 的核心,從大的方面上講,它要完成兩個功能:
通過傳入參數構建動態 SQL 語句;
SQL 語句的執行以及封裝查詢結果集成 List;
5.2.1 參數映射和動態 SQL 語句生成
動態語句生成可以說是 MyBatis 框架非常優雅的一個設計,MyBatis 通過傳入的參數值,使用 Ognl 來動態地構造 SQL 語句,使得 MyBatis 有很強的靈活性和擴展性。
參數映射指的是對於 java 數據類型和 jdbc 數據類型之間的轉換:這裏有包括兩個過程:查詢階段,我們要將 java 類型的數據,轉換成 jdbc 類型的數據,通過 preparedStatement.setXXX() 來設值;另一個就是對 resultset 查詢結果集的 jdbcType 數據轉換成 java 數據類型。
5.2.2 SQL 語句的執行以及封裝查詢結果集成 List###
動態 SQL 語句生成之後,MyBatis 將執行 SQL 語句,並將可能返回的結果集轉換成 List 列表。MyBatis 在對結果集的處理中,支持結果集關係一對多和多對一的轉換,並且有兩種支持方式,一種爲嵌套查詢語句的查詢,還有一種是嵌套結果集的查詢。
5.3 框架支撐層 ##
- 事務管理機制
事務管理機制對於 ORM 框架而言是不可缺少的一部分,事務管理機制的質量也是考量一個 ORM 框架是否優秀的一個標準。
- 連接池管理機制
由於創建一個數據庫連接所佔用的資源比較大,對於數據吞吐量大和訪問量非常大的應用而言,連接池的設計就顯得非常重要。
- 緩存機制
爲了提高數據利用率和減小服務器和數據庫的壓力,MyBatis 會對於一些查詢提供會話級別的數據緩存,會將對某一次查詢,放置到 SqlSession 中,在允許的時間間隔內,對於完全相同的查詢,MyBatis 會直接將緩存結果返回給用戶,而不用再到數據庫中查找。
- SQL 語句的配置方式
傳統的 MyBatis 配置 SQL 語句方式就是使用 XML 文件進行配置的,但是這種方式不能很好地支持面向接口編程的理念,爲了支持面向接口的編程,MyBatis 引入了 Mapper 接口的概念,面向接口的引入,對使用註解來配置 SQL 語句成爲可能,用戶只需要在接口上添加必要的註解即可,不用再去配置 XML 文件了,但是,目前的 MyBatis 只是對註解配置 SQL 語句提供了有限的支持,某些高級功能還是要依賴 XML 配置文件配置 SQL 語句。
5.4 引導層 ##
引導層是配置和啓動 MyBatis 配置信息的方式。MyBatis 提供兩種方式來引導 MyBatis :基於 XML 配置文件的方式和基於 Java API 的方式。
5.5 主要構件及其相互關係 ##
從 MyBatis 代碼實現的角度來看,MyBatis 的主要的核心部件有以下幾個:
**SqlSession:**作爲 MyBatis 工作的主要頂層 API,表示和數據庫交互的會話,完成必要數據庫增刪改查功能;
**Executor:**MyBatis 執行器,是 MyBatis 調度的核心,負責 SQL 語句的生成和查詢緩存的維護;
**StatementHandler:**封裝了 JDBC Statement 操作,負責對 JDBC statement 的操作,如設置參數、將 Statement 結果集轉換成 List 集合。
**ParameterHandler:**負責對用戶傳遞的參數轉換成 JDBC Statement 所需要的參數;
**ResultSetHandler:**負責將 JDBC 返回的 ResultSet 結果集對象轉換成 List 類型的集合;
**TypeHandler:**負責 java 數據類型和 jdbc 數據類型之間的映射和轉換;
**MappedStatement:**MappedStatement 維護了一條 <select|update|delete|insert> 節點的封裝;
**SqlSource:**負責根據用戶傳遞的 parameterObject,動態地生成 SQL 語句,將信息封裝到 BoundSql 對象中,並返回;
**BoundSql:**表示動態生成的 SQL 語句以及相應的參數信息;
**Configuration:**MyBatis 所有的配置信息都維持在 Configuration 對象之中;
它們的關係如下圖所示:
6 SqlSession 工作過程分析#
- 開啓一個數據庫訪問會話 --- 創建 SqlSession 對象
SqlSession sqlSession = factory.openSession();
- 爲 SqlSession 傳遞一個配置的 Sql 語句的 Statement Id 和參數,然後返回結果:
List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);
上述的 "com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",是配置在 EmployeesMapper.xml 的 Statement ID,params 是傳遞的查詢參數。
讓我們來看一下 sqlSession.selectList() 方法的定義:
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//1.根據Statement Id,在mybatis 配置對象Configuration中查找和配置文件相對應的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//2. 將查詢任務委託給MyBatis 的執行器 Executor
List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
MyBatis 在初始化的時候,會將 MyBatis 的配置信息全部加載到內存中,使用 org.apache.ibatis.session.Configuration 實例來維護。使用者可以使用 sqlSession.getConfiguration() 方法來獲取。MyBatis 的配置文件中配置信息的組織格式和內存中對象的組織格式幾乎完全對應的。
上述例子中的:
<select resultMap="BaseResultMap" parameterType="java.util.Map" >
select
EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY
from LOUIS.EMPLOYEES
<if test="min_salary != null">
where SALARY < #{min_salary,jdbcType=DECIMAL}
</if>
</select>
加載到內存中會生成一個對應的 MappedStatement 對象,然後會以 key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary" ,value 爲 MappedStatement 對象的形式維護到 Configuration 的一個 Map 中。當以後需要使用的時候,只需要通過 Id 值來獲取就可以了。
從上述的代碼中我們可以看到 SqlSession 的職能是:SqlSession 根據 Statement ID, 在 mybatis 配置對象 Configuration 中獲取到對應的 MappedStatement 對象,然後調用 mybatis 執行器來執行具體的操作。
- MyBatis 執行器 Executor 根據 SqlSession 傳遞的參數執行 query() 方法(由於代碼過長,讀者只需閱讀我註釋的地方即可):
/**
* BaseExecutor 類部分代碼
*
*/
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1. 根據具體傳入的參數,動態地生成需要執行的SQL語句,用BoundSql對象表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 爲當前的查詢創建一個緩存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) throw new ExecutorException("Executor was closed.");
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 3.緩存中沒有值,直接從數據庫中讀取數據
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear(); // issue #601
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//4. 執行查詢,返回List 結果,然後 將查詢的結果放入緩存之中
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
/**
*
* SimpleExecutor類的doQuery()方法實現
*
*/
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//5. 根據既有的參數,創建StatementHandler對象來執行查詢操作
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//6. 創建java.Sql.Statement對象,傳遞給StatementHandler對象
stmt = prepareStatement(handler, ms.getStatementLog());
//7. 調用StatementHandler.query()方法,返回List結果集
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
上述的 Executor.query() 方法幾經轉折,最後會創建一個 StatementHandler 對象,然後將必要的參數傳遞給 StatementHandler,使用 StatementHandler 來完成對數據庫的查詢,最終返回 List 結果集。
從上面的代碼中我們可以看出,Executor 的功能和作用是:
根據傳遞的參數,完成 SQL 語句的動態解析,生成 BoundSql 對象,供 StatementHandler 使用;
爲查詢創建緩存,以提高性能;
創建 JDBC 的 Statement 連接對象,傳遞給 StatementHandler 對象,返回 List 查詢結果;
- StatementHandler 對象負責設置 Statement 對象中的查詢參數、處理 JDBC 返回的 resultSet,將 resultSet 加工爲 List 集合返回:
接着上面的 Executor 第六步,看一下:prepareStatement() 方法的實現:
/**
*
* SimpleExecutor類的doQuery()方法實現
*
*/
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 1.準備Statement對象,並設置Statement對象的參數
stmt = prepareStatement(handler, ms.getStatementLog());
// 2. StatementHandler執行query()方法,返回List結果
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection);
//對創建的Statement對象設置參數,即設置SQL 語句中 ? 設置爲指定的參數
handler.parameterize(stmt);
return stmt;
}
以上我們可以總結 StatementHandler 對象主要完成兩個工作:
對於 JDBC 的 PreparedStatement 類型的對象,創建的過程中,我們使用的是 SQL 語句字符串會包含 若干個? 佔位符,我們其後再對佔位符進行設值。
StatementHandler 通過 parameterize(statement) 方法對 Statement 進行設值;StatementHandler 通過 List query(Statement statement, ResultHandler resultHandler) 方法來完成執行 Statement,和將 Statement 對象返回的 resultSet 封裝成 List;
- StatementHandler 的 parameterize(statement) 方法的實現:
/**
* StatementHandler 類的parameterize(statement) 方法實現
*/
public void parameterize(Statement statement) throws SQLException {
//使用ParameterHandler對象來完成對Statement的設值
parameterHandler.setParameters((PreparedStatement) statement);
}
/**
*
* ParameterHandler類的setParameters(PreparedStatement ps) 實現
* 對某一個Statement進行設置參數
*/
public void setParameters(PreparedStatement ps) throws SQLException {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 每一個Mapping都有一個TypeHandler,根據TypeHandler來對preparedStatement進行設置參數
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
// 設置參數
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
從上述的代碼可以看到, StatementHandler 的 parameterize(Statement) 方法調用了 ParameterHandler 的 setParameters(statement) 方法,
ParameterHandler 的 setParameters(Statement) 方法負責 根據我們輸入的參數,對 statement 對象的 ? 佔位符處進行賦值。
- StatementHandler 的 List query(Statement statement, ResultHandler resultHandler) 方法的實現:
/**
* PreParedStatement類的query方法實現
*/
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//1.調用preparedStatemnt。execute()方法,然後將resultSet交給ResultSetHandler處理
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
//2. 使用ResultHandler來處理ResultSet
return resultSetHandler.<E> handleResultSets(ps);
}
從上述代碼我們可以看出,StatementHandler 的 List query(Statement statement, ResultHandler resultHandler) 方法的實現,是調用了 ResultSetHandler 的 handleResultSets(Statement) 方法。ResultSetHandler 的 handleResultSets(Statement) 方法會將 Statement 語句執行後生成的 resultSet 結果集轉換成 List 結果集:
/**
* ResultSetHandler類的handleResultSets()方法實現
*
*/
public List<Object> handleResultSets(Statement stmt) throws SQLException {
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
//將resultSet
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResulSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
7 MyBatis 初始化機制#
7.1 MyBatis 的初始化做了什麼 ##
**任何框架的初始化,無非是加載自己運行時所需要的配置信息。**MyBatis 的配置信息,大概包含以下信息,其高層級結構如下:
MyBatis 的上述配置信息會配置在 XML 配置文件中,那麼,這些信息被加載進入 MyBatis 內部,MyBatis 是怎樣維護的呢?
MyBatis 採用了一個非常直白和簡單的方式 --- 使用 org.apache.ibatis.session.Configuration 對象作爲一個所有配置信息的容器,Configuration 對象的組織結構和 XML 配置文件的組織結構幾乎完全一樣(當然,Configuration 對象的功能並不限於此,它還負責創建一些 MyBatis 內部使用的對象,如 Executor 等,這將在後續的文章中討論)。如下圖所示:
MyBatis 根據初始化好 Configuration 信息,這時候用戶就可以使用 MyBatis 進行數據庫操作了。可以這麼說,MyBatis 初始化的過程,就是創建 Configuration 對象的過程。
MyBatis 的初始化可以有兩種方式:
**基於 XML 配置文件:**基於 XML 配置文件的方式是將 MyBatis 的所有配置信息放在 XML 文件中,MyBatis 通過加載並 XML 配置文件,將配置文信息組裝成內部的 Configuration 對象。
**基於 Java API:**這種方式不使用 XML 配置文件,需要 MyBatis 使用者在 Java 代碼中,手動創建 Configuration 對象,然後將配置參數 set 進入 Configuration 對象中。
接下來我們將通過 基於 XML 配置文件方式的 MyBatis 初始化,深入探討 MyBatis 是如何通過配置文件構建 Configuration 對象,並使用它。
7.2 基於 XML 配置文件創建 Configuration 對象 ##
現在就從使用 MyBatis 的簡單例子入手,深入分析一下 MyBatis 是怎樣完成初始化的,都初始化了什麼。看以下代碼:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");
有過 MyBatis 使用經驗的讀者會知道,上述語句的作用是執行 com.foo.bean.BlogMapper.queryAllBlogInfo 定義的 SQL 語句,返回一個 List 結果集。總的來說,上述代碼經歷了 mybatis 初始化 --> 創建 SqlSession --> 執行 SQL 語句返回結果三個過程。
上述代碼的功能是根據配置文件 mybatis-config.xml 配置文件,創建 SqlSessionFactory 對象,然後產生 SqlSession,執行 SQL 語句。而 mybatis 的初始化就發生在第三句:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 現在就讓我們看看第三句到底發生了什麼。
- MyBatis 初始化基本過程:
SqlSessionFactoryBuilder 根據傳入的數據流生成 Configuration 對象,然後根據 Configuration 對象創建默認的 SqlSessionFactory 實例。
初始化的基本過程如下序列圖所示:
由上圖所示,mybatis 初始化要經過簡單的以下幾步:
調用 SqlSessionFactoryBuilder 對象的 build(inputStream) 方法;
SqlSessionFactoryBuilder 會根據輸入流 inputStream 等信息創建 XMLConfigBuilder 對象;
SqlSessionFactoryBuilder 調用 XMLConfigBuilder 對象的 parse() 方法;
XMLConfigBuilder 對象返回 Configuration 對象;
SqlSessionFactoryBuilder 根據 Configuration 對象創建一個 DefaultSessionFactory 對象;
SqlSessionFactoryBuilder 返回 DefaultSessionFactory 對象給 Client,供 Client 使用。
SqlSessionFactoryBuilder 相關的代碼如下所示:
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//2. 創建XMLConfigBuilder對象用來解析XML配置文件,生成Configuration對象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//3. 將XML配置文件內的信息解析成Java對象Configuration對象
Configuration config = parser.parse();
//4. 根據Configuration對象創建出SqlSessionFactory對象
return build(config);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
// 從此處可以看出,MyBatis內部通過Configuration對象來創建SqlSessionFactory,用戶也可以自己通過API構造好Configuration對象,調用此方法創SqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
上述的初始化過程中,涉及到了以下幾個對象:
**SqlSessionFactoryBuilder :**SqlSessionFactory 的構造器,用於創建 SqlSessionFactory,採用了 Builder 設計模式
**Configuration :**該對象是 mybatis-config.xml 文件中所有 mybatis 配置信息
**SqlSessionFactory:**SqlSession 工廠類,以工廠形式創建 SqlSession 對象,採用了 Factory 工廠設計模式
**XMLConfigBuilder :**負責將 mybatis-config.xml 配置文件解析成 Configuration 對象,共 SqlSessonFactoryBuilder 使用,創建 SqlSessionFactory
- 創建 Configuration 對象的過程:
接着上述的 MyBatis 初始化基本過程討論,當 SqlSessionFactoryBuilder 執行 build() 方法,調用了 XMLConfigBuilder 的 parse() 方法,然後返回了 Configuration 對象。那麼 parse() 方法是如何處理 XML 文件,生成 Configuration 對象的呢?
-
(1)XMLConfigBuilder 會將 XML 配置文件的信息轉換爲 Document 對象,而 XML 配置定義文件 DTD 轉換成 XMLMapperEntityResolver 對象,然後將二者封裝到 XpathParser 對象中,XpathParser 的作用是提供根據 Xpath 表達式獲取基本的 DOM 節點 Node 信息的操作。如下圖所示:
XpathParser 組成結構圖和生成圖 -
(2)之後 XMLConfigBuilder 調用 parse() 方法:會從 XPathParser 中取出 節點對應的 Node 對象,然後解析此 Node 節點的子 Node:properties, settings, typeAliases,typeHandlers, objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers:
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; //源碼中沒有這一句,只有parseConfiguration(parser.evalNode("/configuration")); //爲了讓讀者看得更明晰,源碼拆分爲以下兩句 XNode configurationNode = parser.evalNode("/configuration"); parseConfiguration(configurationNode); return configuration; } /** * 解析 "/configuration"節點下的子節點信息,然後將解析的結果設置到Configuration對象中 */ private void parseConfiguration(XNode root) { try { //1.首先處理properties 節點 propertiesElement(root.evalNode("properties")); //issue #117 read properties first //2.處理typeAliases typeAliasesElement(root.evalNode("typeAliases")); //3.處理插件 pluginElement(root.evalNode("plugins")); //4.處理objectFactory objectFactoryElement(root.evalNode("objectFactory")); //5.objectWrapperFactory objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //6.settings settingsElement(root.evalNode("settings")); //7.處理environments environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631 //8.database databaseIdProviderElement(root.evalNode("databaseIdProvider")); //9.typeHandlers typeHandlerElement(root.evalNode("typeHandlers")); //10.mappers mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
注意:在上述代碼中,還有一個非常重要的地方,就是解析 XML 配置文件子節點 的方法 mapperElements(root.evalNode("mappers")), 它將解析我們配置的 Mapper.xml 配置文件,Mapper 配置文件可以說是 MyBatis 的核心,MyBatis 的特性和理念都體現在此 Mapper 的配置和設計上。
-
(3)然後將這些值解析出來設置到 Configuration 對象中:
解析子節點的過程這裏就不一一介紹了,用戶可以參照 MyBatis 源碼仔細揣摩,我們就看上述的 environmentsElement(root.evalNode("environments")); 方法是如何將 environments 的信息解析出來,設置到 Configuration 對象中的:
/** * 解析environments節點,並將結果設置到Configuration對象中 * 注意:創建envronment時,如果SqlSessionFactoryBuilder指定了特定的環境(即數據源); * 則返回指定環境(數據源)的Environment對象,否則返回默認的Environment對象; * 這種方式實現了MyBatis可以連接多數據源 */ private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); if (isSpecifiedEnvironment(id)) { //1.創建事務工廠 TransactionFactory TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); //2.創建數據源DataSource DataSource dataSource = dsFactory.getDataSource(); //3. 構造Environment對象 Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); //4. 將創建的Envronment對象設置到configuration 對象中 configuration.setEnvironment(environmentBuilder.build()); } } } }
private boolean isSpecifiedEnvironment(String id) { if (environment == null) { throw new BuilderException("No environment specified."); } else if (id == null) { throw new BuilderException("Environment requires an id attribute."); } else if (environment.equals(id)) { return true; } return false; }
-
(4)返回 Configuration 對象:
將上述的 MyBatis 初始化基本過程的序列圖細化:
基於 XML 配置創建 Configuration 對象的過程
7.3 基於 Java API 手動加載 XML 配置文件創建 Configuration 對象,並使用 SqlSessionFactory 對象 ##
我們可以使用 XMLConfigBuilder 手動解析 XML 配置文件來創建 Configuration 對象,代碼如下:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 手動創建XMLConfigBuilder,並解析創建Configuration對象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null,null);
Configuration configuration=parse();
// 使用Configuration對象創建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
// 使用MyBatis
SqlSession sqlSession = sqlSessionFactory.openSession();
List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");
7.4 涉及到的設計模式 ##
初始化的過程涉及到創建各種對象,所以會使用一些創建型的設計模式。在初始化的過程中,Builder 模式運用的比較多。
7.4.1 Builder 模式應用 1: SqlSessionFactory 的創建
對於創建 SqlSessionFactory 時,會根據情況提供不同的參數,其參數組合可以有以下幾種:
由於構造時參數不定,可以爲其創建一個構造器 Builder,將 SqlSessionFactory 的構建過程和表示分開:
7.4.2 Builder 模式應用 2: 數據庫連接環境 Environment 對象的創建
在構建 Configuration 對象的過程中,XMLConfigBuilder 解析 mybatis XML 配置文件節點 節點時,會有以下相應的代碼:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
//是和默認的環境相同時,解析之
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
//使用了Environment內置的構造器Builder,傳遞id 事務工廠和數據源
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
在 Environment 內部,定義了靜態內部 Builder 類:
public final class Environment {
private final String id;
private final TransactionFactory transactionFactory;
private final DataSource dataSource;
public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {
if (id == null) {
throw new IllegalArgumentException("Parameter 'id' must not be null");
}
if (transactionFactory == null) {
throw new IllegalArgumentException("Parameter 'transactionFactory' must not be null");
}
this.id = id;
if (dataSource == null) {
throw new IllegalArgumentException("Parameter 'dataSource' must not be null");
}
this.transactionFactory = transactionFactory;
this.dataSource = dataSource;
}
public static class Builder {
private String id;
private TransactionFactory transactionFactory;
private DataSource dataSource;
public Builder(String id) {
this.id = id;
}
public Builder transactionFactory(TransactionFactory transactionFactory) {
this.transactionFactory = transactionFactory;
return this;
}
public Builder dataSource(DataSource dataSource) {
this.dataSource = dataSource;
return this;
}
public String id() {
return this.id;
}
public Environment build() {
return new Environment(this.id, this.transactionFactory, this.dataSource);
}
}
public String getId() {
return this.id;
}
public TransactionFactory getTransactionFactory() {
return this.transactionFactory;
}
public DataSource getDataSource() {
return this.dataSource;
}
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://www.jianshu.com/p/ec40a82cae28