MyBatis 架構與原理深入解析
1 引言
本文主要講解JDBC怎么演變到Mybatis的漸變過程,重點講解了為什么要將JDBC封裝成Mybaits這樣一個持久層框架。再而論述Mybatis作為一個數(shù)據(jù)持久層框架本身有待改進之處。
2 JDBC實現(xiàn)查詢分析
我們先看看我們最熟悉也是最基礎的通過JDBC查詢數(shù)據(jù)庫數(shù)據(jù),一般需要以下七個步驟:
加載JDBC驅(qū)動; 建立并獲取數(shù)據(jù)庫連接; 創(chuàng)建 JDBC Statements 對象; 設置SQL語句的傳入?yún)?shù); 執(zhí)行SQL語句并獲得查詢結果; 對查詢結果進行轉換處理并將處理結果返回; 釋放相關資源(關閉Connection,關閉Statement,關閉ResultSet);
推薦一個開源免費的 Spring Boot 實戰(zhàn)項目:
https://github.com/javastacks/spring-boot-best-practice
以下是具體的實現(xiàn)代碼:
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驅(qū)動
Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
String url = "jdbc:oracle:thin:@localhost:1521:ORACLEDB";
String user = "trainer";
String password = "trainer";
// 獲取數(shù)據(jù)庫連接
connection = DriverManager.getConnection(url,user,password);
String sql = "select * from userinfo where user_id = ? ";
// 創(chuàng)建Statement對象(每一個Statement為一次數(shù)據(jù)庫執(zhí)行請求)
stmt = connection.prepareStatement(sql);
// 設置傳入?yún)?shù)
stmt.setString(1, "zhangsan");
// 執(zhí)行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;
}
// 關閉執(zhí)行
if (stmt != null) {
stmt.close();
stmt = null;
}
if (connection != null) {
connection.close();
connection = null;
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return resultList;
}
3 JDBC演變到Mybatis過程
上面我們看到了實現(xiàn)JDBC有七個步驟,哪些步驟是可以進一步封裝的,減少我們開發(fā)的代碼量。
3.1 第一步優(yōu)化:連接獲取和釋放
-
問題描述:
數(shù)據(jù)庫連接頻繁的開啟和關閉本身就造成了資源的浪費,影響系統(tǒng)的性能。
解決問題:
數(shù)據(jù)庫連接的獲取和關閉我們可以使用數(shù)據(jù)庫連接池來解決資源浪費的問題。通過連接池就可以反復利用已經(jīng)建立的連接去訪問數(shù)據(jù)庫了。減少連接的開啟和關閉的時間。
-
問題描述:
但是現(xiàn)在連接池多種多樣,可能存在變化,有可能采用DBCP的連接池,也有可能采用容器本身的JNDI數(shù)據(jù)庫連接池。
解決問題:
我們可以通過DataSource進行隔離解耦,我們統(tǒng)一從DataSource里面獲取數(shù)據(jù)庫連接,DataSource具體由DBCP實現(xiàn)還是由容器的JNDI實現(xiàn)都可以,所以我們將DataSource的具體實現(xiàn)通過讓用戶配置來應對變化。
3.2 第二步優(yōu)化:SQL統(tǒng)一存取
-
問題描述:
我們使用JDBC進行操作數(shù)據(jù)庫時,SQL語句基本都散落在各個JAVA類中,這樣有三個不足之處:
第一,可讀性很差,不利于維護以及做性能調(diào)優(yōu)。
第二,改動Java代碼需要重新編譯、打包部署。
第三,不利于取出SQL在數(shù)據(jù)庫客戶端執(zhí)行(取出后還得刪掉中間的Java代碼,編寫好的SQL語句寫好后還得通過+號在Java進行拼湊)。
解決問題:
我們可以考慮不把SQL語句寫到Java代碼中,那么把SQL語句放到哪里呢?首先需要有一個統(tǒng)一存放的地方,我們可以將這些SQL語句統(tǒng)一集中放到配置文件或者數(shù)據(jù)庫里面(以key-value的格式存放)。然后通過SQL語句的key值去獲取對應的SQL語句。
既然我們將SQL語句都統(tǒng)一放在配置文件或者數(shù)據(jù)庫中,那么這里就涉及一個SQL語句的加載問題。
3.3 第三步優(yōu)化:傳入?yún)?shù)映射和動態(tài)SQL
-
問題描述:
很多情況下,我們都可以通過在SQL語句中設置占位符來達到使用傳入?yún)?shù)的目的,這種方式本身就有一定局限性,它是按照一定順序傳入?yún)?shù)的,要與占位符一一匹配。但是,如果我們傳入的參數(shù)是不確定的(比如列表查詢,根據(jù)用戶填寫的查詢條件不同,傳入查詢的參數(shù)也是不同的,有時是一個參數(shù)、有時可能是三個參數(shù)),那么我們就得在后臺代碼中自己根據(jù)請求的傳入?yún)?shù)去拼湊相應的SQL語句,這樣的話還是避免不了在Java代碼里面寫SQL語句的命運。既然我們已經(jīng)把SQL語句統(tǒng)一存放在配置文件或者數(shù)據(jù)庫中了,怎么做到能夠根據(jù)前臺傳入?yún)?shù)的不同,動態(tài)生成對應的SQL語句呢?
解決問題:
第一,我們先解決這個動態(tài)問題,按照我們正常的程序員思維是,通過if和else這類的判斷來進行是最直觀的,這個時候我們想到了JSTL中的
這樣的標簽,那么,能不能將這類的標簽引入到SQL語句中呢?假設可以,那么我們這里就需要一個專門的SQL解析器來解析這樣的SQL語句,但是,if判斷的變量來自于哪里呢?傳入的值本身是可變的,那么我們得為這個值定義一個不變的變量名稱,而且這個變量名稱必須和對應的值要有對應關系,可以通過這個變量名稱找到對應的值,這個時候我們想到了key-value的Map。解析的時候根據(jù)變量名的具體值來判斷。
假如前面可以判斷沒有問題,那么假如判斷的結果是true,那么就需要輸出的標簽里面的SQL片段,但是怎么解決在標簽里面使用變量名稱的問題呢?這里我們需要使用一種有別于SQL的語法來嵌入變量(比如使用#變量名#)。這樣,SQL語句經(jīng)過解析后就可以動態(tài)的生成符合上下文的SQL語句。
還有,怎么區(qū)分開占位符變量和非占位變量?有時候我們單單使用占位符是滿足不了的,占位符只能為查詢條件占位,SQL語句其他地方使用不了。這里我們可以使用#變量名#表示占位符變量,使用變量名表示非占位符變量。
3.4 第四步優(yōu)化:結果映射和結果緩存
-
問題描述:
執(zhí)行SQL語句、獲取執(zhí)行結果、對執(zhí)行結果進行轉換處理、釋放相關資源是一整套下來的。假如是執(zhí)行查詢語句,那么執(zhí)行SQL語句后,返回的是一個ResultSet結果集,這個時候我們就需要將ResultSet對象的數(shù)據(jù)取出來,不然等到釋放資源時就取不到這些結果信息了。我們從前面的優(yōu)化來看,以及將獲取連接、設置傳入?yún)?shù)、執(zhí)行SQL語句、釋放資源這些都封裝起來了,只剩下結果處理這塊還沒有進行封裝,如果能封裝起來,每個數(shù)據(jù)庫操作都不用自己寫那么一大堆Java代碼,直接調(diào)用一個封裝的方法就可以搞定了。
解決問題:
我們分析一下,一般對執(zhí)行結果的有哪些處理,有可能將結果不做任何處理就直接返回,也有可能將結果轉換成一個JavaBean對象返回、一個Map返回、一個List返回等`,結果處理可能是多種多樣的。從這里看,我們必須告訴SQL處理器兩點:第一,需要返回什么類型的對象;第二,需要返回的對象的數(shù)據(jù)結構怎么跟執(zhí)行的結果映射,這樣才能將具體的值copy到對應的數(shù)據(jù)結構上。
接下來,我們可以進而考慮對SQL執(zhí)行結果的緩存來提升性能。緩存數(shù)據(jù)都是key-value的格式,那么這個key怎么來呢?怎么保證唯一呢?即使同一條SQL語句幾次訪問的過程中由于傳入?yún)?shù)的不同,得到的執(zhí)行SQL語句也是不同的。那么緩存起來的時候是多對。但是SQL語句和傳入?yún)?shù)兩部分合起來可以作為數(shù)據(jù)緩存的key值。
3.5 第五步優(yōu)化:解決重復SQL語句問題##
-
問題描述:
由于我們將所有SQL語句都放到配置文件中,這個時候會遇到一個SQL重復的問題,幾個功能的SQL語句其實都差不多,有些可能是SELECT后面那段不同、有些可能是WHERE語句不同。有時候表結構改了,那么我們就需要改多個地方,不利于維護。
解決問題:
當我們的代碼程序出現(xiàn)重復代碼時怎么辦?將重復的代碼抽離出來成為獨立的一個類,然后在各個需要使用的地方進行引用。對于SQL重復的問題,我們也可以采用這種方式,通過將SQL片段模塊化,將重復的SQL片段獨立成一個SQL塊,然后在各個SQL語句引用重復的SQL塊,這樣需要修改時只需要修改一處即可。
4 Mybaits有待改進之處#
-
問題描述:
Mybaits所有的數(shù)據(jù)庫操作都是基于SQL語句,導致什么樣的數(shù)據(jù)庫操作都要寫SQL語句。一個應用系統(tǒng)要寫的SQL語句實在太多了。
改進方法:
我們對數(shù)據(jù)庫進行的操作大部分都是對表數(shù)據(jù)的增刪改查,很多都是對單表的數(shù)據(jù)進行操作,由這點我們可以想到一個問題:單表操作可不可以不寫SQL語句,通過JavaBean的默認映射器生成對應的SQL語句,比如:一個類UserInfo對應于USER_INFO表, userId屬性對應于USER_ID字段。這樣我們就可以通過反射可以獲取到對應的表結構了,拼湊成對應的SQL語句顯然不是問題。另外,搜索公眾號Linux就該這樣學后臺回復“猴子”,獲取一份驚喜禮包。
5 MyBatis框架整體設計#
5.1 接口層-和數(shù)據(jù)庫交互的方式
MyBatis和數(shù)據(jù)庫的交互有兩種方式:
使用傳統(tǒng)的MyBatis提供的API; 使用Mapper接口;
5.1.1 使用傳統(tǒng)的MyBatis提供的API###
這是傳統(tǒng)的傳遞Statement Id 和查詢參數(shù)給 SqlSession 對象,使用 SqlSession對象完成和數(shù)據(jù)庫的交互;MyBatis提供了非常方便和簡單的API,供用戶實現(xiàn)對數(shù)據(jù)庫的增刪改查數(shù)據(jù)操作,以及對數(shù)據(jù)庫連接信息和MyBatis 自身配置信息的維護操作。
上述使用MyBatis 的方法,是創(chuàng)建一個和數(shù)據(jù)庫打交道的SqlSession對象,然后根據(jù)Statement Id 和參數(shù)來操作數(shù)據(jù)庫,這種方式固然很簡單和實用,但是它不符合面向?qū)ο笳Z言的概念和面向接口編程的編程習慣。由于面向接口的編程是面向?qū)ο蟮拇筅厔荩琈yBatis 為了適應這一趨勢,增加了第二種使用MyBatis 支持接口(Interface)調(diào)用方式。
5.1.2 使用Mapper接口###
MyBatis 將配置文件中的每一個
這個接口中聲明的方法和
節(jié)點中的<select|update|delete|insert> 節(jié)點項對應 ,即<select|update|delete|insert> 節(jié)點的id值為Mapper 接口中的方法名稱,parameterType 值表示Mapper 對應方法的入?yún)㈩愋?/strong>,而resultMap 值則對應了Mapper 接口表示的返回值類型或者返回結果集的元素類型。
根據(jù)MyBatis 的配置規(guī)范配置好后,通過SqlSession.getMapper(XXXMapper.class)方法,MyBatis 會根據(jù)相應的接口聲明的方法信息,通過動態(tài)代理機制生成一個Mapper 實例,我們使用Mapper接口的某一個方法時,MyBatis會根據(jù)這個方法的方法名和參數(shù)類型,確定Statement Id,底層還是通過SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject); 等等來實現(xiàn)對數(shù)據(jù)庫的操作,MyBatis引用Mapper 接口這種調(diào)用方式,純粹是為了滿足面向接口編程的需要。(其實還有一個原因是在于,面向接口的編程,使得用戶在接口上可以使用注解來配置SQL語句,這樣就可以脫離XML配置文件,實現(xiàn)“0配置”)。
5.2 數(shù)據(jù)處理層##
數(shù)據(jù)處理層可以說是MyBatis的核心,從大的方面上講,它要完成兩個功能:
通過傳入?yún)?shù)構建動態(tài)SQL語句; SQL語句的執(zhí)行以及封裝查詢結果集成List ;
5.2.1 參數(shù)映射和動態(tài)SQL語句生成###
動態(tài)語句生成可以說是MyBatis框架非常優(yōu)雅的一個設計,MyBatis 通過傳入的參數(shù)值,使用 Ognl 來動態(tài)地構造SQL語句,使得MyBatis 有很強的靈活性和擴展性。
參數(shù)映射指的是對于java 數(shù)據(jù)類型和jdbc數(shù)據(jù)類型之間的轉換:這里有包括兩個過程:查詢階段,我們要將java類型的數(shù)據(jù),轉換成jdbc類型的數(shù)據(jù),通過 preparedStatement.setXXX() 來設值;另一個就是對resultset查詢結果集的jdbcType 數(shù)據(jù)轉換成java 數(shù)據(jù)類型。
5.2.2 SQL語句的執(zhí)行以及封裝查詢結果集成List
###
動態(tài)SQL語句生成之后,MyBatis 將執(zhí)行SQL語句,并將可能返回的結果集轉換成List
5.3 框架支撐層##
1、事務管理機制
事務管理機制對于ORM框架而言是不可缺少的一部分,事務管理機制的質(zhì)量也是考量一個ORM框架是否優(yōu)秀的一個標準。
2、連接池管理機制
由于創(chuàng)建一個數(shù)據(jù)庫連接所占用的資源比較大,對于數(shù)據(jù)吞吐量大和訪問量非常大的應用而言,連接池的設計就顯得非常重要。
3、緩存機制
為了提高數(shù)據(jù)利用率和減小服務器和數(shù)據(jù)庫的壓力,MyBatis 會對于一些查詢提供會話級別的數(shù)據(jù)緩存,會將對某一次查詢,放置到SqlSession 中,在允許的時間間隔內(nèi),對于完全相同的查詢,MyBatis會直接將緩存結果返回給用戶,而不用再到數(shù)據(jù)庫中查找。
4、SQL語句的配置方式
傳統(tǒng)的MyBatis 配置SQL語句方式就是使用XML文件進行配置的,但是這種方式不能很好地支持面向接口編程的理念,為了支持面向接口的編程,MyBatis 引入了Mapper接口的概念,面向接口的引入,對使用注解來配置SQL語句成為可能,用戶只需要在接口上添加必要的注解即可,不用再去配置XML文件了,但是,目前的MyBatis 只是對注解配置SQL語句提供了有限的支持,某些高級功能還是要依賴XML配置文件配置SQL 語句。
5.4 引導層##
引導層是配置和啟動MyBatis配置信息的方式。MyBatis 提供兩種方式來引導MyBatis :基于XML配置文件的方式和基于Java API 的方式。
5.5 主要構件及其相互關系##
從MyBatis代碼實現(xiàn)的角度來看,MyBatis的主要的核心部件有以下幾個:
SqlSession:作為MyBatis工作的主要頂層API,表示和數(shù)據(jù)庫交互的會話,完成必要數(shù)據(jù)庫增刪改查功能;
Executor:MyBatis執(zhí)行器,是MyBatis 調(diào)度的核心,負責SQL語句的生成和查詢緩存的維護;
StatementHandler:封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設置參數(shù)、將Statement結果集轉換成List集合。
ParameterHandler:負責對用戶傳遞的參數(shù)轉換成JDBC Statement 所需要的參數(shù);
ResultSetHandler:負責將JDBC返回的ResultSet結果集對象轉換成List類型的集合;
TypeHandler:負責java數(shù)據(jù)類型和jdbc數(shù)據(jù)類型之間的映射和轉換;
MappedStatement:MappedStatement維護了一條<select|update|delete|insert>節(jié)點的封裝;
SqlSource:負責根據(jù)用戶傳遞的parameterObject,動態(tài)地生成SQL語句,將信息封裝到BoundSql對象中,并返回;
BoundSql:表示動態(tài)生成的SQL語句以及相應的參數(shù)信息;
Configuration:MyBatis所有的配置信息都維持在Configuration對象之中;
它們的關系如下圖所示:
6 SqlSession工作過程分析#
-
開啟一個數(shù)據(jù)庫訪問會話---創(chuàng)建SqlSession對象
SqlSession sqlSession = factory.openSession();
MyBatis封裝了對數(shù)據(jù)庫的訪問,把對數(shù)據(jù)庫的會話和事務控制放到了SqlSession對象中
-
為SqlSession傳遞一個配置的Sql語句的Statement Id和參數(shù),然后返回結果:
List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);
上述的"com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",是配置在EmployeesMapper.xml 的Statement ID,params是傳遞的查詢參數(shù)。
讓我們來看一下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.根據(jù)Statement Id,在mybatis 配置對象Configuration中查找和配置文件相對應的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//2. 將查詢?nèi)蝿瘴薪oMyBatis 的執(zhí)行器 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的配置信息全部加載到內(nèi)存中,使用org.apache.ibatis.session.Configuration實例來維護。使用者可以使用sqlSession.getConfiguration()方法來獲取。MyBatis的配置文件中配置信息的組織格式和內(nèi)存中對象的組織格式幾乎完全對應的。
上述例子中的:
<select id="selectByMinSalary" 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>
加載到內(nèi)存中會生成一個對應的MappedStatement對象,然后會以key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary" ,value為MappedStatement對象的形式維護到Configuration的一個Map中。當以后需要使用的時候,只需要通過Id值來獲取就可以了。
從上述的代碼中我們可以看到SqlSession的職能是:SqlSession根據(jù)Statement ID, 在mybatis配置對象Configuration中獲取到對應的MappedStatement對象,然后調(diào)用mybatis執(zhí)行器來執(zhí)行具體的操作。
-
MyBatis執(zhí)行器Executor根據(jù)SqlSession傳遞的參數(shù)執(zhí)行query()方法(由于代碼過長,讀者只需閱讀我注釋的地方即可):
/**
* BaseExecutor 類部分代碼
*
*/
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1. 根據(jù)具體傳入的參數(shù),動態(tài)地生成需要執(zhí)行的SQL語句,用BoundSql對象表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 為當前的查詢創(chuàng)建一個緩存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.緩存中沒有值,直接從數(shù)據(jù)庫中讀取數(shù)據(jù)
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. 執(zhí)行查詢,返回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()方法實現(xiàn)
*
*/
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. 根據(jù)既有的參數(shù),創(chuàng)建StatementHandler對象來執(zhí)行查詢操作
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//6. 創(chuàng)建java.Sql.Statement對象,傳遞給StatementHandler對象
stmt = prepareStatement(handler, ms.getStatementLog());
//7. 調(diào)用StatementHandler.query()方法,返回List結果集
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
上述的Executor.query()方法幾經(jīng)轉折,最后會創(chuàng)建一個StatementHandler對象,然后將必要的參數(shù)傳遞給StatementHandler,使用StatementHandler來完成對數(shù)據(jù)庫的查詢,最終返回List結果集。
從上面的代碼中我們可以看出,Executor的功能和作用是:
根據(jù)傳遞的參數(shù),完成SQL語句的動態(tài)解析,生成BoundSql對象,供StatementHandler使用; 為查詢創(chuàng)建緩存,以提高性能; 創(chuàng)建JDBC的Statement連接對象,傳遞給StatementHandler對象,返回List查詢結果;
-
StatementHandler對象負責設置Statement對象中的查詢參數(shù)、處理JDBC返回的resultSet,將resultSet加工為List 集合返回:
接著上面的Executor第六步,看一下:prepareStatement() 方法的實現(xiàn):
/**
*
* SimpleExecutor類的doQuery()方法實現(xiàn)
*
*/
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對象的參數(shù)
stmt = prepareStatement(handler, ms.getStatementLog());
// 2. StatementHandler執(zhí)行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);
//對創(chuàng)建的Statement對象設置參數(shù),即設置SQL 語句中 ? 設置為指定的參數(shù)
handler.parameterize(stmt);
return stmt;
}
以上我們可以總結StatementHandler對象主要完成兩個工作:
對于JDBC的PreparedStatement類型的對象,創(chuàng)建的過程中,我們使用的是SQL語句字符串會包含 若干個? 占位符,我們其后再對占位符進行設值。StatementHandler通過parameterize(statement)方法對Statement進行設值; StatementHandler通過List query(Statement statement, ResultHandler resultHandler)方法來完成執(zhí)行Statement,和將Statement對象返回的resultSet封裝成List;
-
StatementHandler 的parameterize(statement) 方法的實現(xiàn):
/**
* StatementHandler 類的parameterize(statement) 方法實現(xiàn)
*/
public void parameterize(Statement statement) throws SQLException {
//使用ParameterHandler對象來完成對Statement的設值
parameterHandler.setParameters((PreparedStatement) statement);
}
/**
*
* ParameterHandler類的setParameters(PreparedStatement ps) 實現(xiàn)
* 對某一個Statement進行設置參數(shù)
*/
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,根據(jù)TypeHandler來對preparedStatement進行設置參數(shù)
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
// 設置參數(shù)
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
從上述的代碼可以看到,StatementHandler的parameterize(Statement) 方法調(diào)用了 ParameterHandler的setParameters(statement) 方法,ParameterHandler的setParameters(Statement)方法負責 根據(jù)我們輸入的參數(shù),對statement對象的 ? 占位符處進行賦值。
-
StatementHandler 的List query(Statement statement, ResultHandler resultHandler)方法的實現(xiàn):
/**
* PreParedStatement類的query方法實現(xiàn)
*/
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//1.調(diào)用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)方法的實現(xiàn),是調(diào)用了ResultSetHandler的handleResultSets(Statement) 方法。 ResultSetHandler的handleResultSets(Statement) 方法會將Statement語句執(zhí)行后生成的resultSet 結果集轉換成List 結果集 :
/**
* ResultSetHandler類的handleResultSets()方法實現(xiàn)
*
*/
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配置信息結構圖
MyBatis的上述配置信息會配置在XML配置文件中,那么,這些信息被加載進入MyBatis內(nèi)部,MyBatis是怎樣維護的呢?
MyBatis采用了一個非常直白和簡單的方式---使用 org.apache.ibatis.session.Configuration對象作為一個所有配置信息的容器,Configuration對象的組織結構和XML配置文件的組織結構幾乎完全一樣(當然,Configuration對象的功能并不限于此,它還負責創(chuàng)建一些MyBatis內(nèi)部使用的對象,如Executor等,這將在后續(xù)的文章中討論)。如下圖所示:
Configuration對象的組織結構和XML配置文件的組織結構幾乎完全一樣
MyBatis根據(jù)初始化好Configuration信息,這時候用戶就可以使用MyBatis進行數(shù)據(jù)庫操作了。可以這么說,MyBatis初始化的過程,就是創(chuàng)建 Configuration對象的過程。
MyBatis的初始化可以有兩種方式:
基于XML配置文件:基于XML配置文件的方式是將MyBatis的所有配置信息放在XML文件中,MyBatis通過加載并XML配置文件,將配置文信息組裝成內(nèi)部的Configuration對象。
基于Java API:這種方式不使用XML配置文件,需要MyBatis使用者在Java代碼中,手動創(chuàng)建Configuration對象,然后將配置參數(shù)set 進入Configuration對象中。
接下來我們將通過 基于XML配置文件方式的MyBatis初始化,深入探討MyBatis是如何通過配置文件構建Configuration對象,并使用它。
7.2 基于XML配置文件創(chuàng)建Configuration對象
現(xiàn)在就從使用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使用經(jīng)驗的讀者會知道,上述語句的作用是執(zhí)行com.foo.bean.BlogMapper.queryAllBlogInfo 定義的SQL語句,返回一個List結果集。總的來說,上述代碼經(jīng)歷了mybatis初始化 -->創(chuàng)建SqlSession -->執(zhí)行SQL語句返回結果三個過程。
上述代碼的功能是根據(jù)配置文件mybatis-config.xml 配置文件,創(chuàng)建SqlSessionFactory對象,然后產(chǎn)生SqlSession,執(zhí)行SQL語句。而mybatis的初始化就發(fā)生在第三句:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 現(xiàn)在就讓我們看看第三句到底發(fā)生了什么。
-
MyBatis初始化基本過程:
SqlSessionFactoryBuilder根據(jù)傳入的數(shù)據(jù)流生成Configuration對象,然后根據(jù)Configuration對象創(chuàng)建默認的SqlSessionFactory實例。
初始化的基本過程如下序列圖所示:
MyBatis初始化序列圖
由上圖所示,mybatis初始化要經(jīng)過簡單的以下幾步:
調(diào)用SqlSessionFactoryBuilder對象的build(inputStream)方法; SqlSessionFactoryBuilder會根據(jù)輸入流inputStream等信息創(chuàng)建XMLConfigBuilder對象; SqlSessionFactoryBuilder調(diào)用XMLConfigBuilder對象的parse()方法; XMLConfigBuilder對象返回Configuration對象; SqlSessionFactoryBuilder根據(jù)Configuration對象創(chuàng)建一個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. 創(chuàng)建XMLConfigBuilder對象用來解析XML配置文件,生成Configuration對象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//3. 將XML配置文件內(nèi)的信息解析成Java對象Configuration對象
Configuration config = parser.parse();
//4. 根據(jù)Configuration對象創(chuàng)建出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內(nèi)部通過Configuration對象來創(chuàng)建SqlSessionFactory,用戶也可以自己通過API構造好Configuration對象,調(diào)用此方法創(chuàng)SqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
上述的初始化過程中,涉及到了以下幾個對象:
SqlSessionFactoryBuilder :SqlSessionFactory的構造器,用于創(chuàng)建SqlSessionFactory,采用了Builder設計模式
Configuration :該對象是mybatis-config.xml文件中所有mybatis配置信息
SqlSessionFactory:SqlSession工廠類,以工廠形式創(chuàng)建SqlSession對象,采用了Factory工廠設計模式
XMLConfigBuilder :負責將mybatis-config.xml配置文件解析成Configuration對象,共SqlSessonFactoryBuilder使用,創(chuàng)建SqlSessionFactory
-
創(chuàng)建Configuration對象的過程:接著上述的 MyBatis初始化基本過程討論,當SqlSessionFactoryBuilder執(zhí)行build()方法,調(diào)用了XMLConfigBuilder的parse()方法,然后返回了Configuration對象。那么parse()方法是如何處理XML文件,生成Configuration對象的呢?
-
(1)XMLConfigBuilder會將XML配置文件的信息轉換為Document對象,而XML配置定義文件DTD轉換成XMLMapperEntityResolver對象,然后將二者封裝到XpathParser對象中,XpathParser的作用是提供根據(jù)Xpath表達式獲取基本的DOM節(jié)點Node信息的操作。
如下圖所示:
XpathParser組成結構圖和生成圖
-
(2)之后XMLConfigBuilder調(diào)用parse()方法:會從XPathParser中取出
節(jié)點對應的Node對象,然后解析此Node節(jié)點的子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"節(jié)點下的子節(jié)點信息,然后將解析的結果設置到Configuration對象中
*/
private void parseConfiguration(XNode root) {
try {
//1.首先處理properties 節(jié)點
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配置文件子節(jié)點
的方法mapperElements(root.evalNode("mappers")), 它將解析我們配置的Mapper.xml配置文件,Mapper配置文件可以說是MyBatis的核心 ,MyBatis的特性和理念都體現(xiàn)在此Mapper的配置和設計上。 -
(3)然后將這些值解析出來設置到Configuration對象中:
解析子節(jié)點的過程這里就不一一介紹了,用戶可以參照MyBatis源碼仔細揣摩,我們就看上述的environmentsElement(root.evalNode("environments")); 方法是如何將environments的信息解析出來,設置到Configuration對象中的:
/**
* 解析environments節(jié)點,并將結果設置到Configuration對象中
* 注意:創(chuàng)建envronment時,如果SqlSessionFactoryBuilder指定了特定的環(huán)境(即數(shù)據(jù)源);
* 則返回指定環(huán)境(數(shù)據(jù)源)的Environment對象,否則返回默認的Environment對象;
* 這種方式實現(xiàn)了MyBatis可以連接多數(shù)據(jù)源
*/
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.創(chuàng)建事務工廠 TransactionFactory
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
//2.創(chuàng)建數(shù)據(jù)源DataSource
DataSource dataSource = dsFactory.getDataSource();
//3. 構造Environment對象
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
//4. 將創(chuàng)建的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配置創(chuàng)建Configuration對象的過程
7.3 基于Java API手動加載XML配置文件創(chuàng)建Configuration對象,并使用SqlSessionFactory對象
我們可以使用XMLConfigBuilder手動解析XML配置文件來創(chuàng)建Configuration對象,代碼如下:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 手動創(chuàng)建XMLConfigBuilder,并解析創(chuàng)建Configuration對象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null,null);
Configuration configuration=parse();
// 使用Configuration對象創(chuàng)建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
// 使用MyBatis
SqlSession sqlSession = sqlSessionFactory.openSession();
List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");
7.4 涉及到的設計模式
初始化的過程涉及到創(chuàng)建各種對象,所以會使用一些創(chuàng)建型的設計模式。在初始化的過程中,Builder模式運用的比較多。
7.4.1 Builder模式應用1:SqlSessionFactory的創(chuàng)建###
對于創(chuàng)建SqlSessionFactory時,會根據(jù)情況提供不同的參數(shù),其參數(shù)組合可以有以下幾種:
根據(jù)情況提供不同的參數(shù),創(chuàng)建SqlSessionFactory
由于構造時參數(shù)不定,可以為其創(chuàng)建一個構造器Builder,將SqlSessionFactory的構建過程和表示分開:
MyBatis將SqlSessionFactoryBuilder和SqlSessionFactory相互獨立
7.4.2 Builder模式應用2:數(shù)據(jù)庫連接環(huán)境Environment對象的創(chuàng)建###
在構建Configuration對象的過程中,XMLConfigBuilder解析 mybatis XML配置文件節(jié)點
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");
//是和默認的環(huán)境相同時,解析之
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
//使用了Environment內(nèi)置的構造器Builder,傳遞id 事務工廠和數(shù)據(jù)源
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
在Environment內(nèi)部,定義了靜態(tài)內(nèi)部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;
}
}
作者:七寸知架構來源:https://www.jianshu.com/p/ec40a82cae28

