本篇文章是「深入淺出MyBatis:技術原理與實踐」書籍的總結筆記。
上一篇介紹了反射和動態代理基礎,主要是為本篇文章做個鋪墊,反射使配置和靈活性大大提高,可以給很多配置設定引數,動態代理可以在執行時建立代理物件,做一些特殊的處理。
文章索引:
本篇會介紹MyBatis解析和執行原理,下一篇介紹外掛及應用,目的是更好地編寫外掛,通過本篇的介紹,你會了解到:
- 構建SqlSessionFactory過程
- 對映器的動態代理
- SqlSession的4大物件
- sql執行的過程
SqlSessionFactory和SqlSession是MyBatis的核心元件,在文章 JDBC和MyBatis介紹 中有詳細說明。
構建SqlSessionFactory過程
構建主要分為2步:
- 通過XMLConfigBuilder解析配置的XML檔案,讀出配置引數,包括基礎配置XML檔案和對映器XML檔案;
- 使用Configuration物件建立SqlSessionFactory,SqlSessionFactory是一個介面,提供了一個預設的實現類DefaultSqlSessionFactory。
說白了,就是將我們的所有配置解析為Configuration物件,在整個生命週期內,可以通過該物件獲取需要的配置。
由於外掛需要頻繁訪問對映器的內部組成,會重點這部分,瞭解這塊配置抽象出來的物件:
MappedStatement
它儲存對映器的一個節點(select|insert|delete|update),包括配置的SQL,SQL的id、快取資訊、resultMap、parameterType、resultType等重要配置內容。
它涉及的物件比較多,一般不去修改它。
SqlSource
它是MappedStatement的一個屬性,主要作用是根據引數和其他規則組裝SQL,也是很複雜的,一般也不用修改它。
BoundSql
對於引數和SQL,主要反映在BoundSql類物件上,在外掛中,通過它獲取到當前執行的SQL和引數以及引數規則,作出適當的修改,滿足特殊的要求。
BoundSql提供3個主要的屬性:parameterObject、parameterMappings和sql,下面分別來介紹。
parameterObject為引數本身,可以傳遞簡單物件、POJO、Map或@Param註解的引數:
- 傳遞簡單物件(int、float、String等),會把引數轉換為對應的類,比如int會轉換為Integer;
- 如果傳遞的是POJO或Map,paramterObject就是傳入的POJO或Map不變;
- 如果傳遞多個引數,沒有@Param註解,parameterObject就是一個Map<String,Object>物件,類似這樣的形式{"1":p1 , "2":p2 , "3":p3 ... "param1":p1 , "param2":p2 , "param3",p3 ...},所以在編寫的時候可以使用#{param1}或#{1}去引用第一個引數;
- 如果傳遞多個引數,有@Param註解,與沒有註解的類似,只是將序號的key替換為@Param指定的name;
parameterMappings,它是一個List,元素是ParameterMapping物件,這個物件會描繪sql中的引數引用,包括名稱、表示式、javaType、jdbcType、typeHandler等資訊。
sql,是寫在對映器裡面的一條sql。
有了Configuration物件,構建SqlSessionFactory就簡單了:
sqlSessionFactory = new SqlSessionFactoryBuilder().bulid(inputStream);
複製程式碼
SqlSession執行過程
對映器的動態代理
Mapper對映是通過動態代理來實現的,使用JDK動態代理返回一個代理物件,供呼叫者訪問。
首先看看實現InvocationHandler介面的類,它是執行本代理方法的關鍵,可以看到,Mapper是一個介面,會生成MapperMethod物件,呼叫execute方法。
public class MapperProxy<T> implements InvocationHandler, Serializable {
.....
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
}
複製程式碼
看下面的程式碼,MapperMethod採用命令模式,根據不同的sql操作,做不同的處理。
public class MapperMethod {
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
......
}
}
}
複製程式碼
最後看下,生成代理類的方法,就是使用JDK動態代理Proxy來建立的。
public class MapperProxyFactory<T> {
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
}
複製程式碼
總結下對映器的呼叫過程,返回的Mapper物件是代理物件,當呼叫它的某個方法時,其實是呼叫MapperProxy#invoke方法,而對映器的XML檔案的名稱空間對應的就是這個介面的全路徑,會根據全路徑和方法名,便能夠繫結起來,定位到sql,最後會使用SqlSession介面的方法使它能夠執行查詢。
SqlSession下的四大物件
通過上面的分析,對映器就是一個動態代理物件,進入到了MapperMethod的execute方法,它經過簡單的判斷就進入了SqlSession的刪除、更新、插入、選擇等方法,這些方法如何執行是下面要介紹的內容。
Mapper執行的過程是通過Executor、StatementHandler、ParameterHandler和ResultHandler來完成資料庫操作和結果返回的,理解他們是編寫外掛的關鍵:
- Executor:執行器,由它統一排程其他三個物件來執行對應的SQL;
- StatementHandler:使用資料庫的Statement執行操作;
- ParameterHandler:用於SQL對引數的處理;
- ResultHandler:進行最後資料集的封裝返回處理;
在MyBatis中存在三種執行器:
- SIMPLE:簡易執行器,預設的執行器;
- REUSE:執行重用預處理語句;
- BATCH:執行重用語句和批量更新,針對批量專用的執行器;
以SimpleExecutor為例,說明執行過程
public class SimpleExecutor extends BaseExecutor {
/**
* 執行查詢操作
*/
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(this, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
/**
* 初始化StatementHandler
*/
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection);
handler.parameterize(stmt);
return stmt;
}
/**
* 執行查詢
*/
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.<E>handleResultSets(statement);
}
}
複製程式碼
可以看到最後會委託給StatementHandler會話器進行處理,它是一個介面,實際建立的是RoutingStatementHandler物件,但它不是真實的服務物件,它是通過介面卡模式找到對應的StatementHandler執行的。在MyBatis中,StatementHandler和Executor一樣分為三種:SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler。
Executor會先呼叫StatementHandler的prepare方法預編譯SQL語句,同時設定一些基本執行的引數。然後呼叫parameterize()方法啟用ParameterHandler設定引數,完成預編譯,跟著執行查詢,用ResultHandler封裝結果返回給呼叫者。
引數處理器和結果處理器比較簡單,就不在此介紹了。
下一篇會介紹外掛及其應用,主要是在sql執行的過程中,在四大物件的基礎上進行擴充套件。
歡迎掃描下方二維碼,關注我的個人微信公眾號 ~