前面知道了SqlSession 初始化過程,那麼下一步就看看Mybatis具體的增刪改查邏輯。
本文以以下幾個問題開展:
- Mybatis Mapper 代理對象獲取流程
- 動態sql查詢時,if,foreach等節點是怎麼處理的?
- Mybatis動態Sql對如何讀取參數的?
- @Param註解什麼作用,什麼情況沒有@Param註解會讀取不到參數?
- 對於結果集,Mybatis是如何處理的?
- Mybatis 一級二級緩存如何使用?
Mybatis代理對象
當使用 Mybatis是,我們只需要定義接口,而後定義對應 xml文件,就可以完成增刪改查,接口對象怎麼創建呢?
- 通過SqlSession 的
getMapper
:
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
Configuration
中 的getMapper
:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
MapperRegistry
中保存了來自 xml配置下所有Mappers,並放到一個 Map<Class<?>, MapperProxyFactory<?>>
保存所有接口文件對應的 Mapper
構造器。
- 最後在
MapperRegistry
的 通過 在初始化中構造好的MapperProxyFactory
的newInstance
,生成一個 由MapperProxy
包裝的動態對象。
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
而後通過JDK 反射獲取對應接口:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
以上返回了一個動態代理類,該類實現了 mapperInterface
接口,並且以 MapperProxy
作爲默認包裝代理。
所以當使用 執行代理類方法,首先會進入 MapperProxy
的invoke
方法。
Select 查詢分析
Mybatis 整體流程是比較好理解的,好理解的前提起始是我們都忽略了 其強大的XML解析配置,以及對動態SQL的強大支持。下面看看查詢:
- 執行對應查詢方法,例如
mapper.listAllActivedUsers(actived, list);
- 進入
MapperProxy
的invoke
方法:期間會創建一個MapperMethod
類,用於構建 代表增刪改查的SqlComand
;以及方法簽名包裝類MethodSignature
,裏面放着其返回值,是否返回list,是否返回的Cursor
等。 MapperMethod
中,主要是判斷SqlComand
類型,增刪改還是查,最後返回查詢初的結果。- 當Mybatis 判斷爲查詢多個時候,則會進入
MapperMethod
的excuteForMany
方法,這裏面重要的代碼是Object param = method.convertArgsToSqlCommandParam(args);
,param是解析 參數後的 Map。
它會將傳入參數以<key,value>
存儲,以@Param
的值作爲key,並且還會固定以參數順序存儲一份param1, param2, param3
等固定名字參數,所以param
中容量是實際傳入的一倍 - 判斷是否有傳入
RowBounds
分頁參數,有則加載進入。
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
...
- 在
DefaultSqlSession
對其進行 查詢,其中會如果參數是collection
或者數組,則會進行封裝一層。 - 動態sql 拼裝邏輯看下一節
- 查詢時,首先會執行
CachingExecutor
的query
,這一層主要對 一級緩存進行相應邏輯判斷,一級緩存默認開啓:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 判斷 MappedStatement 是否有緩存
Cache cache = ms.getCache();
if (cache != null) {
// 有cache 判斷是否要刪除緩存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 從 TransactionalCacheManager 加載緩存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 查詢後加入緩存
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
-
而後拿着句柄就往下執行
BaseExecutor
的query
方法, 在 裏面有個 對ErrorContext
進行的設定,關於ErrorContext
可以看下下節。 -
如果第一次 查詢,則會清除
localCache
和localOutputParameterCache
緩存,而下面對該次查詢進行了重入性判斷,當再次查詢時則會直接從localCache
獲取。
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
-
下一步即從數據庫中查詢,默認使用
SimpleExecutor
進行 操作,應此Statement
也是一次性使用
@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, 也包括其中對過濾器鏈的操作
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 執行 prepare,即預檢查
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
prepareStatement:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
- 最後就是在
PreparedStatementHandler
執行 sql,執行完後 使用DefaultResultSetHandler.handleResultSets
處理結果。 - 處理查詢結果主要將 查出結果
ResultSet
封裝成結果返回,中途還會 處理 自定義ResultSet
以及Lazy Loading
相關。下面基於DefaultResultSetHandler
的handleResultSets
:
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
// 設置當前動作
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
// 設置檢索
int resultSetCount = 0;
// 從 Statment 中獲取 ResultSet 並封裝
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 獲取ResultMap
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
// 計算resultMaps 數量
int resultMapCount = resultMaps.size();
// 簡單校驗result map 數量
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
// 獲取resultMap
ResultMap resultMap = resultMaps.get(resultSetCount);
// 處理結果成 resultMap形式,並放入multipleResult
handleResultSet(rsw, resultMap, multipleResults, null);
// 下一個resultSetWrapper
rsw = getNextResultSet(stmt);
// 將當前nestedResultObjects 內容
cleanUpAfterHandlingResultSet();
// 如果有則下一個
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
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++;
}
}
// 封裝成list返回
return collapseSingleResultList(multipleResults);
}
動態SQL解析
下一個關鍵點就在 Mybatis的ongl表達式解析了,或許會有這樣一個問題,爲什麼傳入的參數mybatis可以理解,並且可以以動態sql形式解析,支持 if 和 foreach 等邏輯?
在 MappedStatement
的 getBoundSql
可以獲取一個BoundSql
,BoundSql
對象是一個從 SqlSource
中分析之後獲取的 動態對象。最終 能從 xml 對象中將 動態sql解析出含有 ?
和 參數的字段。
- 從 sqlSource中獲取對象:
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
- 而後,在
DynamicSqlSource
中的getBoundSql
進行進一步解析,在DynamicSqlSource
中有變量SqlNode
, 動態sql主要組裝邏輯就在SqlNode
子類中。
上面 SqlNode大致作用如下:
StaticTextSqlNode
: 裏面有個String
類型text
,主要是匹配xml中靜態sqlMixedSqlNode
: 裏面有List<SqlNode>
,主要是裝載多種混合類型SqlNode
,即可以爲多種SqlNode
的組合TextSqlNode
:用於存儲解析 包含${}
佔位符的動態SQL節點。將動態SQL,帶${}
解析完成SQL語句的解析其,即將${}
佔位符替換成實際的變量值。ForEachSqlNode
:用於存儲解析<foreach>
節點值。IfSqlNode
: 用於解析<if>
節點值,由於<if>
下面可能又是一個節點,所以裏面還有一個SqlNode contents
。VarDeclSqlNode
:處理動態xml標籤<bind>
的SqlNode類。TrimSqlNode
: 用於處理<trim>
節點 的 SqlNode。ChooseSqlNode
: 用於處理<choose>
標籤的工具類。
DynamicSqlSource 的 getBoundSql:
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 遞歸的處理動態sql節點
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
上面代碼中,使用 rootSqlNode.apply(context);
解析動態sql節點,而後使用 SqlSourceBuilder
,將 #
開頭的佔位符,全部使用 ?
替代,而後將解析出來參數設置到上下文 DynamicContext
中。
另外,對於處理 <foreach>
的邏輯,是將所有參數都以 ?
填充,而增加相應個數參數從而可以適配循環參數。
ErrorContext 作用
當使用Mybatis時候,是否會發現這樣的感覺,如果哪個環節出錯,能夠迅速的找出問題,或者sql少寫了一個點,或者 哪個 <if>
標籤判斷出錯導致參數錯誤,或者是參數或者結果解析出問題,都能馬上定位到問題。
這不是你厲害,而是Mybatis告訴你哪裏出錯了>-<
Mybatis就是使用ErrorContext 來幫你記錄你sql運行的過程。
例如 : ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
private ErrorContext stored;
private String resource;
private String activity;
private String object;
private String message;
private String sql;
private Throwable cause;
看到 static
的 ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
是一個很典型的 ThreadLocal
用法,每一次與數據庫交互的查詢,都能夠被當前上下文保存下來。
ErrorContext
的構造方法是私有的,使用 instance
創建而成的 單例模式。
resource
:說明是加載哪一個資源,主要爲 xml的Mapper文件activity
:說明什麼動作,例如查詢就是executing a query
object
:說明是執行mapper文件中,哪個語句,例如anla.learn.mybatis.interceptor.dao.UserMapper.listAllActivedUsers
message
: 用於保存出錯信息,調用點爲ExceptionFactory
,而傳入的message主要爲 特定錯誤類型:
sql
:存入boundSql
中的sql,也就是通過動態sql解析過後的sql。在BaseStatementHandler
中的prepare
調用,所以所有與數據庫交互語句都會在這裏初始化 sql。cause
: 主要存儲錯誤棧,配合 上面message
使用。
在每次執行完並且沒有錯,都會將當前 ErrorContext
清除。
一級緩存和二級緩存
其實一級緩存二級緩存命名比較彆扭,因爲事實上,如果開啓了二級緩存,則最終緩存調用順序爲:
二級緩存->一級緩存->數據庫。因爲二級默認不開啓,所以一般是一級緩存,如果一級緩存中沒有,則到數據庫。
一二級緩存這樣區分,主要是Mybatis上有相關注釋說明了 TransactionalCacheManager
爲二級緩存。
- 一級緩存:MyBatis提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提高性能。
- 二級緩存:
二級緩存存儲位置是CachingExecutor
的TransactionalCacheManager
中,默認不開啓。
下面簡單分析下一二級緩存及其原理:
一級緩存
一級緩存在BaseExecutor
的 PerpetualCache
中,默認開啓。作用域默認是 SESSION,可以選擇爲STATEMENT。如果爲SESSION,則在整個SqlSession中都會共享該緩存,而如果爲Statement,則本次查詢結束,則會清除該緩存。
@Override
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 {
// 直接查詢數據庫
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
// 如果是STATEMENT 域緩存,則用完直接清空
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
一級緩存實現原理較爲簡單, PerpetualCache
裏面主要 定義一個 Map<Object, Object> cache
類型全局變量,實現了緩存操作。
總結:
- 一級緩存默認開啓,默認爲 SESSION級別
- Mybatis 一級緩存聲明週期和 SqlSession一致,數據只在 SqlSession內部(配置爲SESSION時)
- Mybatis一級緩存設計爲一個沒有容量的HashMap。
- 以及緩存範圍爲SqlSession,在多個SqlSession或者分佈式下,會引起髒數據,建議設定爲 STATEMENT。
二級緩存
二級緩存主要使用實際上在一級緩存之前就會調用,在 CachingExecutor
的 query
中,但是需要設置Setting:
<setting name="cacheEnabled" value="true"/>
實際在 3.5.2 中, 這個默認是開啓。
第二個是需要在xml中增加 <cache/>
標籤。並配置好namespace
從而在 讀取配置時,會初始化 MappedStatement
中Cache,在 CachingExecutor
中會進行引用:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 如果配置了 二級緩存以及相應namespace,則會有Cache
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 沒有則需要進行查詢
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
二級緩存開啓後,同一個namespace下的所有操作語句,都影響着同一個Cache,即二級緩存被多個SqlSession共享,是一個全局的變量。
覺得博主寫的有用,不妨關注博主公衆號: 六點A君。
哈哈哈,一起研究Mybatis: