Mybatis原始碼詳解系列(三)--從Mapper介面開始看Mybatis的執行邏輯
簡介
Mybatis 是一個持久層框架,它對 JDBC 進行了高階封裝,使我們的程式碼中不會出現任何的 JDBC 程式碼,另外,它還通過 xml 或註解的方式將 sql 從 DAO/Repository 層中解耦出來,除了這些基本功能外,它還提供了動態 sql、延遲載入、快取等功能。 相比 Hibernate,Mybatis 更面向資料庫,可以靈活地對 sql 語句進行優化。
本文繼續分析 Mybatis 的原始碼,第1點內容上一篇部落格已經講過,本文將針對 2 和 3 點繼續分析:
- 載入配置、初始化
SqlSessionFactory
; - 獲取
SqlSession
和Mapper
; - 執行
Mapper
除了原始碼分析,本系列還包含 Mybatis 的詳細使用方法、高階特性、生成器等,相關內容可以我的專欄 Mybatis 。
注意,考慮可讀性,文中部分原始碼經過刪減。
隱藏在Mapper背後的東西
從使用者的角度來看,專案中使用 Mybatis 時,我們只需要定義Mapper
介面和編寫 xml,除此之外,不需要去使用 Mybatis 的其他東西。當我們呼叫了 Mapper 介面的方法,Mybatis 悄無聲息地為我們完成引數設定、語句執行、結果對映等等工作,這真的是相當優秀的設計。
既然是分析原始碼,就必須搞清楚隱藏 Mapper 介面背後都是什麼東西。這裡我畫了一張 UML 圖,通過這張圖,應該可以對 Mybatis 的架構及 Mapper 方法的執行過程形成比較巨集觀的瞭解。
針對上圖,我再簡單梳理下:
Mapper
和SqlSession
可以認為是使用者的入口(專案中也可以不用Mapper
介面,直接使用SqlSession
),Mybatis 為我們生產的Mapper
實現類最終都會去呼叫SqlSession
的方法;Executor
作為整個執行流程的排程者,它依賴StatementHandler
來完成引數設定、語句執行和結果對映,使用Transaction
來管理事務。StatementHandler
呼叫ParameterHandler
為語句設定引數,呼叫ResultSetHandler
將結果集對映為所需物件。
那麼,我們開始看原始碼吧。
Mapper代理類的獲取
一般情況下,我們會先拿到SqlSession
物件,然後再利用SqlSession
獲取Mapper
物件,這部分的原始碼也是按這個順序開展。
// 獲取 SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 獲取 Mapper
EmployeeMapper baseMapper = sqlSession.getMapper(EmployeeMapper.class);
先拿到SqlSession物件
SqlSession的獲取過程
上一篇部落格講了DefaultSqlSessionFactory
的初始化,現在我們將利用DefaultSqlSessionFactory
來建立SqlSession
,這個過程也會創建出對應的Executor
和Transaction
,如下圖所示。
圖中的SqlSession
建立時需要先建立Executor
,而Executor
又要依賴Transaction
的建立,Transaction
則需要依賴已經初始化好的TransactionFactory
和DataSource
。
進入到DefaultSqlSessionFactory.openSession()
方法。預設情況下,SqlSession
是執行緒不安全的,主要和Transaction
物件有關,如果考慮複用SqlSession
物件的話,需要重寫Transaction
的實現。
@Override
public SqlSession openSession() {
// 預設會使用SimpltExecutors,以及autoCommit=false,事務隔離級別為空
// 當然我們也可以在入參指定
// 補充:SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(PreparedStatement);BATCH 執行器不僅重用語句還會執行批量更新。
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 獲取Environment中的TransactionFactory和DataSource,用來建立事務物件
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 建立執行器,這裡也會給執行器安裝外掛
final Executor executor = configuration.newExecutor(tx, execType);
// 建立DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
如何給執行器安裝外掛
上面的程式碼比較簡單,這裡重點說下安裝外掛的過程。我們進入到Configuration.newExecutor(Transaction, ExecutorType)
,可以看到建立完執行器後,還需要給執行器安裝外掛,接下來就是要分析下如何給執行器安裝外掛。
protected final InterceptorChain interceptorChain = new InterceptorChain();
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 根據executorType選擇建立不同的執行器
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 安裝外掛
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
進入到InterceptorChain.pluginAll(Object)
方法,這是一個相當通用的方法,不是隻能給Executor
安裝外掛,後面我們看到的StatementHandler
、ResultSetHandler
、ParameterHandler
等都會被安裝外掛。
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
// 遍歷安裝所有執行器
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// 進入到Interceptor.plugin(Object)方法,這個是接口裡的方法,使用 default 宣告
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
在定義外掛時,一般我們都會採用註解來指定需要攔截的介面及其方法,如下。安裝外掛的方法之所以能夠通用,主要還是@Signature
註解的功勞,註解中,我們已經明確了攔截哪個介面的哪個方法。注意,這裡我也可以定義其他介面,例如StatementHandler
。
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// do something
}
}
上面這個外掛將對Executor
介面的以下兩個方法進行攔截:
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
那麼,Mybatis 是如何實現的呢?我們進入到Plugin
這個類,它是一個InvocationHandler
,也就是說 Mybatis 使用的是 JDK 的動態代理來實現外掛功能,後面程式碼中, JDK 的動態代理也會經常出現。
public class Plugin implements InvocationHandler {
// 需要被安裝外掛的類
private final Object target;
// 需要安裝的外掛
private final Interceptor interceptor;
// 存放外掛攔截的介面接對應的方法
private final Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
// 根據外掛的註解獲取外掛攔截哪些介面哪些方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 獲取目標類中需要被攔截的所有介面
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 建立代理類
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 以“當前方法的宣告類”為key,查詢需要被外掛攔截的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
// 如果包含當前方法,那麼會執行外掛裡的intercept方法
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
// 如果並不包含當前方法,則直接執行該方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
以上就是獲取SqlSession
和安裝外掛的內容。預設情況下,SqlSession
是執行緒不安全的,不斷地建立SqlSession
也不是很明智的做法,按道理,Mybatis 應該提供執行緒安全的一套SqlSession
實現才對。
再獲取Mapper代理類
Mapper 代理類的獲取過程比較簡單,這裡我們就不一步步看原始碼了,直接看圖就行。我畫了 UML 圖,通過這個圖基本可以梳理以下幾個類的關係,繼而明白獲取 Mapper 代理類的方法呼叫過程,另外,我們也能知道,Mapper 代理類也是使用 JDK 的動態代理生成。
Mapper 作為一個使用者介面,最終還是得呼叫SqlSession
來進行增刪改查,所以,代理類也必須持有對SqlSession
的引用。通常情況下,這樣的 Mapper
代理類是執行緒不安全的,因為它持有的SqlSession
實現類DefaultSqlSession
也是執行緒不安全的,但是,如果實現類是SqlSessionManager
就另當別論了。
Mapper方法的執行
執行Mapper代理方法
因為Mapper
代理類是通過 JDK 的動態代理生成,當呼叫Mapper
代理類的方法時,對應的InvocationHandler
物件(即MapperProxy
)將被呼叫,所以,這裡就不展示Mapper
代理類的程式碼了,直接從MapperProxy
這個類開始分析。
同樣地,還是先看看整個 UML 圖,通過圖示大致可以梳理出方法的呼叫過程。MethodSignature
這個類可以重點看下,它的屬性非常關鍵。
下面開始看原始碼,進入到MapperProxy.invoke(Object, Method, Object[])
。這裡的MapperMethodInvoker
物件會被快取起來,因為這個類是無狀態的,不需要反覆的建立。當快取中沒有對應的MapperMethodInvoker
時,方法對應的MapperMethodInvoker
實現類將被建立並放入快取,同時MapperMethod
、MethodSignature
、sqlCommand
等物件都會被建立好。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果是Object類宣告的方法,直接呼叫
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 先從快取拿到MapperMethodInvoker物件,再呼叫它的方法
// 因為最終會呼叫SqlSession的方法,所以這裡得傳入SqlSession物件
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// 快取有就拿,沒有需建立並放入快取
return methodCache.computeIfAbsent(method, m -> {
// 如果是介面中定義的default方法,建立MapperMethodInvoker實現類DefaultMethodInvoker
// 這種情況我們不關注
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 如果不是介面中定義的default方法,建立MapperMethodInvoker實現類PlainMethodInvoker,在此之前也會建立MapperMethod
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
由於我們不考慮DefaultMethodInvoker
的情況,所以,這裡直接進入到MapperProxy.PlainMethodInvoker.invoke(Object, Method, Object[], SqlSession)
。
private final MapperMethod mapperMethod;
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// 直接呼叫MapperMethod的方法,method和proxy的入參丟棄
return mapperMethod.execute(sqlSession, args);
}
進入到MapperMethod.execute(SqlSession, Object[])
方法。前面提到過 Mapper 代理類必須依賴SqlSession
物件來進行增刪改查,在這個方法就可以看到,方法中會通過方法的型別來決定呼叫SqlSession
的哪個方法。
在進行引數轉換時有三種情況:
-
如果引數為空,則 param 為 null;
-
如果引數只有一個且不包含
Param
註解,則 param 就是該入參物件; -
如果引數大於一個或包含了
Param
註解,則 param 是一個Map<String, Object>
,key 為註解Param
的值,value 為對應入參物件。
另外,針對 insert|update|delete 方法,Mybatis 支援使用 void、Integer/int、Long/long、Boolean/boolean 的返回型別,而針對 select 方法,支援使用 Collection、Array、void、Map、Cursor、Optional 返回型別,並且支援入參 RowBounds 來進行分頁,以及入參 ResultHandler 來處理返回結果。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 判斷屬於哪種型別,來決定呼叫SqlSession的哪個方法
switch (command.getType()) {
// 如果為insert型別方法
case INSERT: {
// 引數轉換
Object param = method.convertArgsToSqlCommandParam(args);
// rowCountResult將根據返回型別來處理result,例如,當返回型別為boolean時影響的rowCount是否大於0,當返回型別為int時,直接返回rowCount
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
// 如果為update型別方法
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
// 如果為delete型別方法
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
// 如果為select型別方法
case SELECT:
// 返回void,且入參有ResultHandler
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
// 返回型別為陣列或List
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
// 返回型別為Map
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
// 返回型別為Cursor
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
// 這種一般是返回單個實體物件或者Optional物件
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
// 如果為FLUSH型別方法,這種情況不關注
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
// 當方法返回型別為基本型別,但是result卻為空,這種情況會丟擲異常
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
增刪改的我們不繼續看了,至於查的,只看method.returnsMany()
的情況。進入到MapperMethod.executeForMany(SqlSession, Object[])
。通過這個方法可以知道,當返回多個物件時,Mapper 中我們可以使用List
接收,也可以使用陣列或者Collection
的其他子類來接收,但是處於效能考慮,如果不是必須,建議還是使用List
比較好。
將RowBounds
作為 Mapper 方法的入參,可以支援自動分頁功能,但是,這種方式存在一個很大缺點,就是 Mybatis 會將所有結果查放入本地記憶體再進行分頁,而不是查的時候嵌入分頁引數。所以,這個分頁入參,建議還是不要使用了。
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
// 轉換引數
Object param = method.convertArgsToSqlCommandParam(args);
// 如果入參包含RowBounds物件,這個一般用於分頁使用
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
// 不用分頁的情況
result = sqlSession.selectList(command.getName(), param);
}
// 如果SqlSession方法的返回型別和Mapper方法的返回型別不一致
// 例如,mapper返回型別為陣列、Collection的其他子類
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
// 如果mapper方法要求返回陣列
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
// 如果要求返回Set等Collection子類,這個方法感興趣的可以研究下,非常值得借鑑學習
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
從Mapper進入到SqlSession
接下來就需要進入SqlSession
的方法了,這裡選用實現類DefaultSqlSession
進行分析。SqlSession
作為使用者入口,程式碼不會太多,主要工作還是通過執行器來完成。
在呼叫執行器方法之前,這裡會對引數物件再次包裝,一般針對入參只有一個引數且不包含Param
註解的情況:
- 如果是
Collection
子類,將轉換為放入"collection"=object
鍵值對的 map,如果它是List
的子類,還會再放入"list"=object
的鍵值對 - 如果是陣列,將轉換為放入
"array"=object
鍵值對的 map
@Override
public <E> List<E> selectList(String statement, Object parameter) {
// 這裡還是給它傳入了一個分頁物件,這個物件預設分頁引數為0,Integer.MAX_VALUE
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 利用方法id從配置物件中拿到MappedStatement物件
MappedStatement ms = configuration.getMappedStatement(statement);
// 接著執行器開始排程,傳入resultHandler為空
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
執行器開始排程
接下來就進入執行器的部分了。注意,由於本文不會涉及到 Mybatis 結果快取的內容,所以,下面的程式碼都會刪除快取相關的部分。
那麼,還是回到最開始的圖,接下來將選擇SimpleExecutor
進行分析。
進入到BaseExecutor.query(MappedStatement, Object, RowBounds, ResultHandler)
。這個方法中會根據入參將動態語句轉換為靜態語句,並生成對應的ParameterMapping
。
例如,
<if test="con.gender != null">and e.gender = {con.gender}</if>
將被轉換為 and e.gender = ?
,並且生成
ParameterMapping{property='con.gender', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
的ParameterMapping
。
生成的ParameterMapping
將根據?
的索引放入集合中待使用。
這部分內容我就不展開了,感興趣地可以自行研究。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 將sql語句片段中的動態部分轉換為靜態,並生成對應的ParameterMapping
BoundSql boundSql = ms.getBoundSql(parameter);
// 生成快取的key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
接下來一大堆關於結果快取的程式碼,前面說過,本文不講,所以我們直接跳過進入到SimpleExecutor.doQuery(MappedStatement, Object, RowBounds, ResultHandler, BoundSql)
。可以看到,接下來的任務都是由StatementHandler
來完成,包括了引數設定、語句執行和結果集對映等。
@Override
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物件,在上面的UML中可以看到,它非常重要
// 建立StatementHandler物件時,會根據StatementType自行判斷選擇SimpleStatementHandler、PreparedStatementHandler還是CallableStatementHandler實現類
// 另外,還會給它安裝執行器的所有外掛
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 獲取Statement物件,並設定引數
stmt = prepareStatement(handler, ms.getStatementLog());
// 執行語句,並對映結果集
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
本文將對以下兩行程式碼分別展開分析。其中,關於引數設定的內容不會細講,更多地精力會放在結果集對映上面。
// 獲取Statement物件,並設定引數
stmt = prepareStatement(handler, ms.getStatementLog());
// 執行語句,並對映結果集
return handler.query(stmt, resultHandler);
語句處理器開始處理語句
在建立StatementHandler
時,會通過MappedStatement.getStatementType()
自動選擇使用哪種語句處理器,有以下情況:
- 如果是 STATEMENT,則選擇
SimpleStatementHandler
; - 如果是 PREPARED,則選擇
PreparedStatementHandler
; - 如果是 CALLABLE,則選擇
CallableStatementHandler
; - 其他情況丟擲異常。
本文將選用PreparedStatementHandler
進行分析。
獲取語句物件和設定引數
進入到SimpleExecutor.prepareStatement(StatementHandler, Log)
。這個方法將會獲取當前語句的PreparedStatement
物件,並給它設定引數。
protected Transaction transaction;
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 獲取連線物件,通過Transaction獲取
Connection connection = getConnection(statementLog);
// 獲取Statement物件,由於分析的是PreparedStatementHandler,所以會返回實現類PreparedStatement
stmt = handler.prepare(connection, transaction.getTimeout());
// 設定引數
handler.parameterize(stmt);
return stmt;
}
進入到PreparedStatementHandler.parameterize(Statement)
。正如 UML 圖中說到的,這裡實際上是呼叫ParameterHandler
來設定引數。
protected final ParameterHandler parameterHandler;
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
進入到DefaultParameterHandler.setParameters(PreparedStatement)
。前面講過,在將動態語句轉出靜態語句時,生成了語句每個?
對應的ParameterMapping
,並且這些ParameterMapping
會按照語句中對應的索引被放入集合中。在以下方法中,就是遍歷這個集合,將引數設定到PreparedStatement
中去。
private final TypeHandlerRegistry typeHandlerRegistry;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final Configuration configuration;
@Override
public void setParameters(PreparedStatement ps) {
// 獲得當前語句對應的ParameterMapping
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
// 遍歷ParameterMapping
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// 一般情況mode都是IN,至於OUT的情況,用於結果對映到入參,比較少用
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;// 用於設定到ps中的?的引數
// 這個propertyName對應mapper中#{value}的名字
String propertyName = parameterMapping.getProperty();
// 判斷additionalParameters是否有這個propertyName,這種情況暫時不清楚
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;
// 如果沒有當前入參的型別處理器,這種一般是傳入實體物件或傳入Map的情況
} else {
// 這個原理和前面說過的MetaClass差不多
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
// 如果未指定jdbcType,且入參為空,沒有在setting中配置jdbcTypeForNull的話,預設為OTHER
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 利用型別處理器給ps設定引數
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
使用ParameterHandler
設定引數的內容就不再多講,接下來分析語句執行和結果集對映的程式碼。
語句執行和結果集對映
進入到PreparedStatementHandler.query(Statement, ResultHandler)
方法。語句執行就是普通的 JDBC,沒必要多講,重點看看如何使用ResultSetHandler
完成結果集的對映。
protected final ResultSetHandler resultSetHandler;
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 直接執行
ps.execute();
// 對映結果集
return resultSetHandler.handleResultSets(ps);
}
為了滿足多種需求,Mybatis 在處理結果集對映的邏輯非常複雜,這裡先簡單說下。
一般我們的 resultMap 是這樣配置的:
<resultMap id="blogResult" type="Blog">
<association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
然而,Mybatis 竟然也支援多 resultSet 對映的情況,這裡拿到的第二個結果集將使用resultSet="authors"
的resultMap 進行對映,並將得到的Author
設定進Blog
的屬性。
<resultMap id="blogResult" type="Blog">
<id property="id" column="id" />
<result property="title" column="title"/>
<association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="bio" column="bio"/>
</association>
</resultMap>
<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
{call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>
還有一種更加奇葩的,多 resultSet、多 resultMap,如下。這種我暫時也不清楚怎麼用。
<select id="selectBlogs" resultSets="blogs01,blogs02" resultMap="blogResult01,blogResult02">
{call getTwoBlogs(#{id,jdbcType=INTEGER,mode=IN})}
</select>
接下來只考慮第一種情況。另外兩種感興趣的自己研究吧。
進入到DefaultResultSetHandler.handleResultSets(Statement)
方法。
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
// 用於存放最終物件的集合
final List<Object> multipleResults = new ArrayList<>();
// resultSet索引
int resultSetCount = 0;
// 獲取第一個結果集
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 獲取當前語句對應的所有ResultMap
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
// resultMap總數
int resultMapCount = resultMaps.size();
// 校驗結果集非空時resultMapCount是否為空
validateResultMapsCount(rsw, resultMapCount);
// 接下來結果集和resultMap會根據索引一對一地對映
while (rsw != null && resultMapCount > resultSetCount) {
// 獲取與當前結果集對映的resultMap
ResultMap resultMap = resultMaps.get(resultSetCount);
// 對映結果集,並將生成的物件放入multipleResults
handleResultSet(rsw, resultMap, multipleResults, null);
// 獲取下一個結果集
rsw = getNextResultSet(stmt);
// TODO
cleanUpAfterHandlingResultSet();
// resultSet索引+1
resultSetCount++;
}
// 如果當前resultSet的索引小於resultSets中配置的resultSet數量,將繼續對映
// 這就是前面說的第二種情況了,這個不講
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
// 獲取指定resultSet對應的ResultMap
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
// 獲取巢狀ResultMap進行對映
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
// 獲取下一個結果集
rsw = getNextResultSet(stmt);
// TODO
cleanUpAfterHandlingResultSet();
// resultSet索引+1
resultSetCount++;
}
}
// 如果multipleResults只有一個,返回multipleResults.get(0),否則整個multipleResults一起返回
return collapseSingleResultList(multipleResults);
}
進入DefaultResultSetHandler.handleResultSet(ResultSetWrapper, ResultMap, List<Object>, ResultMapping)
。這裡的入參 parentMapping 一般為空,除非在語句中設定了多個 resultSet;
屬性 resultHandler 一般為空,除非在 Mapper 方法的入參中傳入,這個物件可以由使用者自己實現,通過它我們可以對結果進行操作。在實際專案中,我們往往是拿到實體物件後才到 Web 層完成 VO 物件的轉換,通過ResultHandler
,我們在 DAO 層就能完成 VO 物件的轉換,相比傳統方式,這裡可以減少一次集合遍歷,而且,因為可以直接傳入ResultHandler
,而不是具體實現,所以轉換過程不會滲透到 DAO層。注意,採用這種方式時,Mapper 的返回型別必須為 void。
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
// 如果不是設定了多個resultSets,parentMapping一般為空
// 所以,這種情況不關注
if (parentMapping != null) {
// 對映結果集
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
// resultHandler一般為空
if (resultHandler == null) {
// 建立defaultResultHandler
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
// 對映結果集
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
// 將物件放入集合
multipleResults.add(defaultResultHandler.getResultList());
} else {
// 對映結果集,如果傳入了自定義的ResultHandler,則由使用者自己處理對映好的物件
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
進入到DefaultResultSetHandler.handleRowValues(ResultSetWrapper, ResultMap, ResultHandler<?>, RowBounds, ResultMapping)
。這裡會根據是否包含巢狀結果對映來判斷呼叫哪個方法,如果是巢狀結果對映,需要判斷是否允許分頁,以及是否允許使用自定義ResultHandler
。
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
// 如果resultMap存在巢狀結果對映
if (resultMap.hasNestedResultMaps()) {
// 如果設定了safeRowBoundsEnabled=true,需校驗在巢狀語句中使用分頁時丟擲異常
ensureNoRowBounds();
// 如果設定了safeResultHandlerEnabled=false,需校驗在巢狀語句中使用自定義結果處理器時丟擲異常
checkResultHandler();
// 對映結果集
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
// 如果resultMap不存在巢狀結果對映
} else {
// 對映結果集
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
這裡我就不搞那麼複雜了,就只看非巢狀結果的情況。進入DefaultResultSetHandler.handleRowValuesForSimpleResultMap(ResultSetWrapper, ResultMap, ResultHandler<?>, RowBounds, ResultMapping)
。在這個方法中可以看到,使用RowBounds
進行分頁時,Mybatis 會查出所有資料到記憶體中,然後再分頁,所以,不建議使用。
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
// 建立DefaultResultContext物件,這個用來做標誌判斷使用,還可以作為ResultHandler處理結果的入參
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
// 獲取當前結果集
ResultSet resultSet = rsw.getResultSet();
// 剔除分頁offset以下資料
skipRows(resultSet, rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
// 如果存在discriminator,則根據結果集選擇匹配的resultMap,否則直接返回當前resultMap
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
// 建立實體物件,並完成結果對映
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
// 一般會回撥ResultHandler的handleResult方法,讓使用者可以對對映好的結果進行處理
// 如果配置了resultSets的話,且當前在對映子結果集,那麼會將子結果集對映到的物件設定到父物件的屬性中
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
進入DefaultResultSetHandler.getRowValue(ResultSetWrapper, ResultMap, String)
方法。這個方法將建立物件,並完成結果集的對映。點到為止,有空再做補充了。
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 建立實體物件,這裡會完成構造方法中引數的對映,以及完成懶載入的代理
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
// MetaObject可以方便完成實體物件的獲取和設定屬性
final MetaObject metaObject = configuration.newMetaObject(rowValue);
// foundValues用於標識當前物件是否還有未對映完的屬性
boolean foundValues = this.useConstructorMappings;
// 對映列名和屬性名一致的屬性,如果設定了駝峰規則,那麼這部分也會對映
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
// 對映property的RsultMapping
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
// 返回對映好的物件
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
以上,基本講完 Mapper 的獲取及方法執行相關原始碼的分析。
通過分析 Mybatis 的原始碼,希望讀者能夠更瞭解 Mybatis,從而在實際工作和學習中更好地使用,以及避免“被坑”。針對結果快取、引數設定以及其他細節問題,本文沒有繼續展開,後續有空再補充吧。
參考資料
Mybatis官方中文文件
相關原始碼請移步:mybatis-demo
本文為原創文章,轉載請附上原文出處連結:https://www.cnblogs.com/ZhangZiSheng001/p/12761376.html