該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋(Mybatis原始碼分析 GitHub 地址、Mybatis-Spring 原始碼分析 GitHub 地址、Spring-Boot-Starter 原始碼分析 GitHub 地址)進行閱讀
MyBatis 版本:3.5.2
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
MyBatis的SQL執行過程
在前面一系列的文件中,我已經分析了 MyBatis 的基礎支援層以及整個的初始化過程,此時 MyBatis 已經處於就緒狀態了,等待使用者發號施令了
那麼接下來我們來看看它執行SQL的整個過程,該過程比較複雜,涉及到二級快取,將返回結果轉換成 Java 物件以及延遲載入等等處理過程,這裡將一步一步地進行分析:
- 《SQL執行過程(一)之Executor》
- 《SQL執行過程(二)之StatementHandler》
- 《SQL執行過程(三)之ResultSetHandler》
- 《SQL執行過程(四)之延遲載入》
MyBatis中SQL執行的整體過程如下圖所示:
在 SqlSession 中,會將執行 SQL 的過程交由Executor
執行器去執行,過程大致如下:
- 通過
DefaultSqlSessionFactory
建立與資料庫互動的SqlSession
“會話”,其內部會建立一個Executor
執行器物件 - 然後
Executor
執行器通過StatementHandler
建立對應的java.sql.Statement
物件,並通過ParameterHandler
設定引數,然後執行資料庫相關操作 - 如果是資料庫更新操作,則可能需要通過
KeyGenerator
先設定自增鍵,然後返回受影響的行數 - 如果是資料庫查詢操作,則需要將資料庫返回的
ResultSet
結果集物件包裝成ResultSetWrapper
,然後通過DefaultResultSetHandler
對結果集進行對映,最後返回 Java 物件
上面還涉及到一級快取、二級快取和延遲載入等其他處理過程
SQL執行過程(一)之Executor
在MyBatis的SQL執行過程中,Executor執行器擔當著一個重要的角色,相關操作都需要通過它來執行,相當於一個排程器,把SQL語句交給它,它來呼叫各個元件執行操作
其中一級快取和二級快取都是在Executor執行器中完成的
Executor
執行器介面的實現類如下圖所示:
-
org.apache.ibatis.executor.BaseExecutor
:實現Executor介面,提供骨架方法,支援一級快取,指定幾個抽象的方法交由不同的子類去實現 -
org.apache.ibatis.executor.SimpleExecutor
:繼承 BaseExecutor 抽象類,簡單的 Executor 實現類(預設) -
org.apache.ibatis.executor.ReuseExecutor
:繼承 BaseExecutor 抽象類,可重用的 Executor 實現類,相比SimpleExecutor,在Statement執行完操作後不會立即關閉,而是快取起來,執行的SQL作為key,下次執行相同的SQL時優先從快取中獲取Statement物件 -
org.apache.ibatis.executor.BatchExecutor
:繼承 BaseExecutor 抽象類,支援批量執行的 Executor 實現類 -
org.apache.ibatis.executor.CachingExecutor
:實現 Executor 介面,支援二級快取的 Executor 的實現類,實際採用了裝飾器模式,裝飾物件為左邊三個Executor類
Executor
org.apache.ibatis.executor.Executor
:執行器介面,程式碼如下:
public interface Executor {
/**
* ResultHandler 空物件
*/
ResultHandler NO_RESULT_HANDLER = null;
/**
* 更新或者插入或者刪除
* 由傳入的 MappedStatement 的 SQL 所決定
*/
int update(MappedStatement ms, Object parameter) throws SQLException;
/**
* 查詢,帶 ResultHandler + CacheKey + BoundSql
*/
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey cacheKey, BoundSql boundSql) throws SQLException;
/**
* 查詢,帶 ResultHandler
*/
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException;
/**
* 查詢,返回 Cursor 遊標
*/
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
/**
* 刷入批處理語句
*/
List<BatchResult> flushStatements() throws SQLException;
/**
* 提交事務
*/
void commit(boolean required) throws SQLException;
/**
* 回滾事務
*/
void rollback(boolean required) throws SQLException;
/**
* 建立 CacheKey 物件
*/
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
/**
* 判斷是否快取
*/
boolean isCached(MappedStatement ms, CacheKey key);
/**
* 清除本地快取
*/
void clearLocalCache();
/**
* 延遲載入
*/
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
/**
* 獲得事務
*/
Transaction getTransaction();
/**
* 關閉事務
*/
void close(boolean forceRollback);
/**
* 判斷事務是否關閉
*/
boolean isClosed();
/**
* 設定包裝的 Executor 物件
*/
void setExecutorWrapper(Executor executor);
}
執行器介面定義了運算元據庫的相關方法:
- 資料庫的讀和寫操作
- 事務相關
- 快取相關
- 設定延遲載入
- 設定包裝的 Executor 物件
BaseExecutor
org.apache.ibatis.executor.BaseExecutor
:實現Executor介面,提供骨架方法,指定幾個抽象的方法交由不同的子類去實現,例如:
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds,
BoundSql boundSql) throws SQLException;
上面這四個方法交由不同的子類去實現,分別是:更新資料庫、刷入批處理語句、查詢資料庫和查詢資料返回遊標
構造方法
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
/**
* 事務物件
*/
protected Transaction transaction;
/**
* 包裝的 Executor 物件
*/
protected Executor wrapper;
/**
* DeferredLoad(延遲載入)佇列
*/
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
/**
* 本地快取,即一級快取,內部就是一個 HashMap 物件
*/
protected PerpetualCache localCache;
/**
* 本地輸出型別引數的快取,和儲存過程有關
*/
protected PerpetualCache localOutputParameterCache;
/**
* 全域性配置
*/
protected Configuration configuration;
/**
* 記錄當前會話正在查詢的數量
*/
protected int queryStack;
/**
* 是否關閉
*/
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
}
其中上面的屬性可根據註釋進行檢視
這裡提一下localCache
屬性,本地快取,用於一級快取,MyBatis的一級快取是什麼呢?
每當我們使用 MyBatis 開啟一次和資料庫的會話,MyBatis 都會建立出一個 SqlSession 物件,表示與資料庫的一次會話,而每個 SqlSession 都會建立一個 Executor 物件
在對資料庫的一次會話中,我們有可能會反覆地執行完全相同的查詢語句,每一次查詢都會訪問一次資料庫,如果在極短的時間內做了完全相同的查詢,那麼它們的結果極有可能完全相同,由於查詢一次資料庫的代價很大,如果不採取一些措施的話,可能造成很大的資源浪費
為了解決這一問題,減少資源的浪費,MyBatis 會在每一次 SqlSession 會話物件中建立一個簡單的快取,將每次查詢到的結果快取起來,當下次查詢的時候,如果之前已有完全一樣的查詢,則會先嚐試從這個簡單的快取中獲取結果返回給使用者,不需要再進行一次資料庫查詢了 ? 注意,這個“簡單的快取”就是一級快取,且預設開啟,無法“關閉”
如下圖所示,MyBatis 的一次會話:在一個 SqlSession 會話物件中建立一個
localCache
本地快取,對於每一次查詢,都會根據查詢條件嘗試去localCache
本地快取中獲取快取資料,如果存在,就直接從快取中取出資料然後返回給使用者,否則訪問資料庫進行查詢,將查詢結果存入快取並返回給使用者(如果設定的快取區域為STATEMENT,預設為SESSION,在一次會話中所有查詢執行後會清空當前 SqlSession 會話中的localCache
本地快取,相當於“關閉”了一級快取)所有的資料庫更新操作都會清空當前 SqlSession 會話中的本地快取
如上描述,MyBatis的一級快取在多個 SqlSession 會話時,可能導致資料的不一致性,某一個 SqlSession 更新了資料而其他 SqlSession 無法獲取到更新後的資料,出現資料不一致性,這種情況是不允許出現了,所以我們通常選擇“關閉”一級快取
clearLocalCache方法
clearLocalCache()
方法,清空一級(本地)快取,如果全域性配置中設定的localCacheScope
快取區域為STATEMENT
(預設為SESSION
),則在每一次查詢後會呼叫該方法,相當於關閉了一級快取,程式碼如下:
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
createCacheKey方法
createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql)
方法,根據本地查詢的相關資訊建立一個CacheKey
快取key物件,程式碼如下:
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// <1> 建立 CacheKey 物件
CacheKey cacheKey = new CacheKey();
// <2> 設定 id、offset、limit、sql 到 CacheKey 物件中
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
// <3> 設定 ParameterMapping 陣列的元素對應的每個 value 到 CacheKey 物件中
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) { // 該引數需要作為入參
Object value;
String propertyName = parameterMapping.getProperty();
/*
* 獲取該屬性值
*/
if (boundSql.hasAdditionalParameter(propertyName)) {
// 從附加引數中獲取
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
// 入參物件為空則直接返回 null
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
// 入參有對應的型別處理器則直接返回該引數
value = parameterObject;
} else {
// 從入參物件中獲取該屬性的值
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
// <4> 設定 Environment.id 到 CacheKey 物件中
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
-
建立一個
CacheKey
例項物件 -
將入參中的
id
、offset
、limit
、sql
,通過CacheKey
的update
方法新增到其中,它的方法如下:public void update(Object object) { // 方法引數 object 的 hashcode int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); this.count++; // checksum 為 baseHashCode 的求和 this.checksum += baseHashCode; // 計算新的 hashcode 值 baseHashCode *= this.count; this.hashcode = this.multiplier * this.hashcode + baseHashCode; // 新增 object 到 updateList 中 this.updateList.add(object); }
-
獲取本次查詢的入參值,通過
CacheKey
的update
方法新增到其中 -
獲取本次環境的
Environment.id
,通過CacheKey
的update
方法新增到其中 -
返回
CacheKey
例項物件,這樣就可以為本次查詢生成一個唯一的快取key物件,可以看看CacheKey
重寫的equal
方法:@Override public boolean equals(Object object) { if (this == object) { return true; } if (!(object instanceof CacheKey)) { return false; } final CacheKey cacheKey = (CacheKey) object; if (hashcode != cacheKey.hashcode) { return false; } if (checksum != cacheKey.checksum) { return false; } if (count != cacheKey.count) { return false; } for (int i = 0; i < updateList.size(); i++) { Object thisObject = updateList.get(i); Object thatObject = cacheKey.updateList.get(i); if (!ArrayUtil.equals(thisObject, thatObject)) { return false; } } return true; }
query相關方法
查詢資料庫因為涉及到一級快取,所以這裡有多層方法,最終訪問資料庫的doQuery
方法是交由子類去實現的,總共分為三層:
-
根據入參獲取BoundSql和CacheKey物件,然後再去呼叫查詢方法
-
涉及到一級快取和延遲載入的處理,快取未命中則再去呼叫查詢資料庫的方法
-
儲存一些資訊供一級快取使用,內部呼叫
doQuery
方法執行資料庫的讀操作
接下來我們分別來看看這三個方法
① query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
方法,資料庫查詢操作的入口,程式碼如下
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException {
// <1> 獲得 BoundSql 物件
BoundSql boundSql = ms.getBoundSql(parameter);
// <2> 建立 CacheKey 物件
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// <3> 查詢
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
- 通過
MappedStatement
物件根據入參獲取BoundSql
物件,在《MyBatis初始化(四)之SQL初始化(下)》中的SqlSource小節中有講到這個方法,如果是動態SQL則需要進行解析,獲取到最終的SQL,替換成?
佔位符 - 呼叫
createCacheKey
方法為本次查詢建立一個CacheKey
物件 - 繼續呼叫
query(...)
方法執行查詢
② query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
方法,處理資料庫查詢操作,涉及到一級快取,程式碼如下:
@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());
// <1> 已經關閉,則丟擲 ExecutorException 異常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// <2> 清空本地快取,如果 queryStack 為零,並且要求清空本地快取(配置了 flushCache = true)
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// <3> queryStack + 1
queryStack++;
// <4> 從一級快取中,獲取查詢結果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) { // <4.1> 獲取到,則進行處理
// 處理快取儲存過程的結果
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else { // <4.2> 獲得不到,則從資料庫中查詢
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
// <5> queryStack - 1
queryStack--;
}
if (queryStack == 0) { // <6> 如果當前會話的所有查詢執行完了
// <6.1> 執行延遲載入
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
// <6.2> 清空 deferredLoads
deferredLoads.clear();
// <6.3> 如果快取級別是 LocalCacheScope.STATEMENT ,則進行清理
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
// <7> 返回查詢結果
return list;
}
-
當前會話已經被關閉則丟擲異常
-
如果
queryStack
為0
(表示是當前會話只有本次查詢而沒有其他的查詢了),並且要求清空本地快取(配置了flushCache=true
),那麼直接清空一級(本地)快取 -
當前會話正在查詢的數量加一,
queryStack++
-
從
localCache
一級快取獲取快取的查詢結果- 如果有快取資料,則需要處理儲存過程的情況,將需要作為出參(
OUT
)的引數設定到本次查詢的入參的屬性中 - 如果沒有快取資料,則呼叫
queryFromDatabase
方法,執行資料庫查詢操作
- 如果有快取資料,則需要處理儲存過程的情況,將需要作為出參(
-
當前會話正在查詢的數量減一,
queryStack--
-
如果當前會話所有查詢都執行完
-
執行當前會話中的所有的延遲載入
deferredLoads
,這種延遲載入屬於查詢後的延遲,和後續講到的獲取屬性時再載入不同,這裡的延遲載入是在哪裡生成的呢?在
DefaultResultSetHandler
中進行結果對映時,如果某個屬性配置的是子查詢,並且本次的子查詢在一級快取中有快取資料,那麼將會建立一個DeferredLoad
物件儲存在deferredLoads
中,該屬性值先設定為DEFERRED
延遲載入物件(final修飾的Object物件),待當前會話所有的查詢結束後,也就是當前執行步驟,則會從一級快取獲取到資料設定到返回結果中 -
清空所有的延遲載入
deferredLoads
物件 -
如果全域性配置的快取級別為STATEMENT(預設為SESSION),則清空當前會話中一級快取的所有資料
-
-
返回查詢結果
③ queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
方法,執行資料庫查詢操作,程式碼如下:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// <1> 在快取中,新增正在執行的佔位符物件,因為正在執行的查詢不允許提前載入需要延遲載入的屬性,可見 DeferredLoad#canLoad() 方法
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// <2> 執行讀操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// <3> 從快取中,移除佔位物件
localCache.removeObject(key);
}
// <4> 新增到快取中
localCache.putObject(key, list);
// <5> 如果是儲存過程,則將入參資訊儲存儲存,跟一級快取處理儲存過程相關
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
// <6> 返回查詢結果
return list;
}
- 在快取中,新增正在執行的
EXECUTION_PLACEHOLDER
佔位符物件,因為正在執行的查詢不允許提前載入需要延遲載入的屬性,可見 DeferredLoad#canLoad() 方法 - 呼叫查詢資料庫
doQuery
方法,該方法交由子類實現 - 刪除第
1
步新增的佔位符 - 將查詢結果新增到
localCache
一級快取中 - 如果是儲存過程,則將入參資訊儲存儲存,跟一級快取處理儲存過程相關,可見上面的第
②
個方法的第4.1
步 - 返回查詢結果
update方法
update(MappedStatement ms, Object parameter)
方法,執行更新資料庫的操作,程式碼如下:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
// <1> 已經關閉,則丟擲 ExecutorException 異常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// <2> 清空本地快取
clearLocalCache();
// <3> 執行寫操作
return doUpdate(ms, parameter);
}
- 當前會話已經被關閉則丟擲異常
- 清空當前會話中一級快取的所有資料
- 呼叫更新資料庫
doUpdate
方法,該方法交由子類實現
其他方法
除了上面介紹的幾個重要的方法以外,還有其他很多方法,例如獲取當前事務,提交事務,回滾事務,關閉會話等等,這裡我就不一一列出來了,請自行閱讀該類
SimpleExecutor
org.apache.ibatis.executor.SimpleExecutor
:繼承 BaseExecutor 抽象類,簡單的 Executor 實現類(預設使用)
-
每次對資料庫的操作,都會建立對應的Statement物件
-
執行完成後,關閉該Statement物件
程式碼如下:
public class SimpleExecutor extends BaseExecutor {
public SimpleExecutor(Configuration configuration, Transaction transaction) {
super(configuration, transaction);
}
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 建立 StatementHandler 物件
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
// 初始化 Statement 物件
stmt = prepareStatement(handler, ms.getStatementLog());
// 通過 StatementHandler 執行寫操作
return handler.update(stmt);
} finally {
// 關閉 Statement 物件
closeStatement(stmt);
}
}
@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);
// 初始化 Statement 物件
stmt = prepareStatement(handler, ms.getStatementLog());
// 通過 StatementHandler 執行讀操作
return handler.query(stmt, resultHandler);
} finally {
// 關閉 Statement 物件
closeStatement(stmt);
}
}
@Override
protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
Cursor<E> cursor = handler.queryCursor(stmt);
stmt.closeOnCompletion();
return cursor;
}
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) {
return Collections.emptyList();
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 獲得 Connection 物件,如果開啟了 Debug 模式,則返回的是一個代理物件
Connection connection = getConnection(statementLog);
// 建立 Statement 或 PrepareStatement 物件
stmt = handler.prepare(connection, transaction.getTimeout());
// 往 Statement 中設定 SQL 語句上的引數,例如 PrepareStatement 的 ? 佔位符
handler.parameterize(stmt);
return stmt;
}
}
我們看到這些方法的實現,其中的步驟差不多都是一樣的
-
獲取
Configuration
全域性配置物件 -
通過上面全域性配置物件的
newStatementHandler
方法,建立RoutingStatementHandler
物件,採用了裝飾器模式,根據配置的StatementType
建立對應的物件,預設為PreparedStatementHandler
物件,進入BaseStatementHandler
的構造方法你會發現有幾個重要的步驟,在後續會講到?然後使用外掛鏈對該物件進行應用,方法如下所示:
// Configuration.java public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { /* * 建立 RoutingStatementHandler 路由物件 * 其中根據 StatementType 建立對應型別的 Statement 物件,預設為 PREPARED * 執行的方法都會路由到該物件 */ StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 將 Configuration 全域性配置中的所有外掛應用在 StatementHandler 上面 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
-
呼叫
prepareStatement
方法初始化Statement
物件- 從事務中獲取一個
Connection
資料庫連線,如果開啟了Debug模式,則會為該Connection
建立一個動態代理物件的例項,用於列印Debug日誌 - 通過上面第
2
步建立的StatementHandler
物件建立一個Statement
物件(預設為PrepareStatement
),還會進行一些準備工作,例如:如果配置了KeyGenerator
(設定主鍵),則會設定需要返回相應自增鍵,在後續會講到? - 往
Statement
物件中設定SQL的引數,例如PrepareStatement
的?
佔位符,實際上是通過DefaultParameterHandler
設定佔位符引數,在前面的《MyBatis初始化(四)之SQL初始化(下)》中有講到 - 返回已經建立好的
Statement
物件,就等待著執行資料庫操作了
- 從事務中獲取一個
-
通過
StatementHandler
對Statement
進行資料庫的操作,如果是查詢操作則會通過DefaultResultSetHandler
進行引數對映(非常複雜,後續逐步分析?)
ReuseExecutor
org.apache.ibatis.executor.ReuseExecutor
:繼承 BaseExecutor 抽象類,可重用的 Executor 實現類
- 每次對資料庫的操作,優先從當前會話的快取中獲取對應的Statement物件,如果不存在,才進行建立,建立好了會放入快取中
- 資料庫操作執行完成後,不關閉該Statement物件
- 其它的和SimpleExecutor是一致的
我們來看看他的prepareStatement
方法就好了:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
/*
* 根據需要執行的 SQL 語句判斷 是否已有對應的 Statement 並且連線未關閉
*/
if (hasStatementFor(sql)) {
// 從快取中獲得 Statement 物件
stmt = getStatement(sql);
// 重新設定事務超時時間
applyTransactionTimeout(stmt);
} else {
// 獲得 Connection 物件
Connection connection = getConnection(statementLog);
// 初始化 Statement 物件
stmt = handler.prepare(connection, transaction.getTimeout());
// 將 Statement 新增到快取中,key 值為 當前執行的 SQL 語句
putStatement(sql, stmt);
}
// 往 Statement 中設定 SQL 語句上的引數,例如 PrepareStatement 的 ? 佔位符
handler.parameterize(stmt);
return stmt;
}
在建立Statement
物件前,會根據本次查詢的SQL從本地的Map<String, Statement> statementMap
獲取到對應的Statement
物件
-
如果快取命中,並且該物件的連線未關閉,那麼重新設定當前事務的超時時間
-
如果快取未命中,則執行和
SimpleExecutor
中的prepareStatement
方法相同邏輯建立一個Statement
物件並放入statementMap
快取中
BatchExecutor
org.apache.ibatis.executor.BatchExecutor
:繼承 BaseExecutor 抽象類,支援批量執行的 Executor 實現類
-
我們在執行資料庫的更新操作時,可以通過
Statement
的addBatch()
方法將資料庫操作新增到批處理中,等待呼叫Statement
的executeBatch()
方法進行批處理 -
BatchExecutor
維護了多個Statement
物件,一個物件對應一個SQL(sql
和MappedStatement
物件都相等),每個Statement
物件對應多個資料庫操作(同一個sql
多種入參),就像蘋果藍裡裝了很多蘋果,番茄藍裡裝了很多番茄,最後,再統一倒進倉庫
由於JDBC不支援資料庫查詢的批處理,所以這裡就不展示它資料庫查詢的實現方法,和SimpleExecutor一致,我們來看看其他的方法
構造方法
public class BatchExecutor extends BaseExecutor {
public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
/**
* Statement 陣列
*/
private final List<Statement> statementList = new ArrayList<>();
/**
* BatchResult 陣列
*
* 每一個 BatchResult 元素,對應 {@link #statementList} 集合中的一個 Statement 元素
*/
private final List<BatchResult> batchResultList = new ArrayList<>();
/**
* 上一次新增至批處理的 Statement 物件對應的SQL
*/
private String currentSql;
/**
* 上一次新增至批處理的 Statement 物件對應的 MappedStatement 物件
*/
private MappedStatement currentStatement;
public BatchExecutor(Configuration configuration, Transaction transaction) {
super(configuration, transaction);
}
}
-
statementList
屬性:維護多個Statement
物件 -
batchResultList
屬性:維護多個BatchResult
物件,每個物件對應上面的一個Statement
物件,每個BatchResult
物件包含同一個SQL和其每一次操作的入參 -
currentSql
屬性:上一次新增至批處理的Statement
物件對應的SQL -
currentStatement
屬性:上一次新增至批處理的Statement
物件對應的MappedStatement
物件
BatchResult
org.apache.ibatis.executor.BatchResult
:相同SQL(sql
和MappedStatement
物件都相等)聚合的結果,包含了同一個SQL每一次操作的入參,程式碼如下:
public class BatchResult {
/**
* MappedStatement 物件
*/
private final MappedStatement mappedStatement;
/**
* SQL
*/
private final String sql;
/**
* 引數物件集合
*
* 每一個元素,對應一次操作的引數
*/
private final List<Object> parameterObjects;
/**
* 更新數量集合
*
* 每一個元素,對應一次操作的更新數量
*/
private int[] updateCounts;
public BatchResult(MappedStatement mappedStatement, String sql) {
super();
this.mappedStatement = mappedStatement;
this.sql = sql;
this.parameterObjects = new ArrayList<>();
}
public BatchResult(MappedStatement mappedStatement, String sql, Object parameterObject) {
this(mappedStatement, sql);
addParameterObject(parameterObject);
}
public void addParameterObject(Object parameterObject) {
this.parameterObjects.add(parameterObject);
}
}
doUpdate方法
更新資料庫的操作,新增至批處理,需要呼叫doFlushStatements
執行批處理,程式碼如下:
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
final Configuration configuration = ms.getConfiguration();
// <1> 建立 StatementHandler 物件
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
final Statement stmt;
// <2> 如果和上一次新增至批處理 Statement 物件對應的 currentSql 和 currentStatement 都一致,則聚合到 BatchResult 中
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
// <2.1> 獲取上一次新增至批處理 Statement 物件
int last = statementList.size() - 1;
stmt = statementList.get(last);
// <2.2> 重新設定事務超時時間
applyTransactionTimeout(stmt);
// <2.3> 往 Statement 中設定 SQL 語句上的引數,例如 PrepareStatement 的 ? 佔位符
handler.parameterize(stmt);// fix Issues 322
// <2.4> 獲取上一次新增至批處理 Statement 對應的 BatchResult 物件,將本次的入參新增到其中
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else { // <3> 否則,建立 Statement 和 BatchResult 物件
// <3.1> 初始化 Statement 物件
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt); // fix Issues 322
// <3.2> 設定 currentSql 和 currentStatemen
currentSql = sql;
currentStatement = ms;
// <3.3> 新增 Statement 到 statementList 中
statementList.add(stmt);
// <3.4> 建立 BatchResult 物件,並新增到 batchResultList 中
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// <4> 新增至批處理
handler.batch(stmt);
// <5> 返回 Integer.MIN_VALUE + 1002
return BATCH_UPDATE_RETURN_VALUE;
}
-
建立
StatementHandler
物件,和SimpleExecutor
中一致,在後續會講到? -
如果和上一次新增至批處理
Statement
物件對應的currentSql
和currentStatement
都一致,則聚合到BatchResult
中- 獲取上一次新增至批處理
Statement
物件 - 重新設定事務超時時間
- 往
Statement
中設定 SQL 語句上的引數,例如PrepareStatement
的?
佔位符,在SimpleExecutor
中已經講到 - 獲取上一次新增至批處理
Statement
對應的BatchResult
物件,將本次的入參新增到其中
- 獲取上一次新增至批處理
-
否則,建立
Statement
和BatchResult
物件- 初始化
Statement
物件,在SimpleExecutor
中已經講到,這裡就不再重複了 - 設定
currentSql
和currentStatemen
屬性 - 新增
Statement
到statementList
集合中 - 建立
BatchResult
物件,並新增到batchResultList
集合中
- 初始化
-
新增至批處理
-
返回
Integer.MIN_VALUE + 1002
,為什麼返回這個值?不清楚
doFlushStatements方法
執行批處理,也就是將之前新增至批處理的資料庫更新操作進行批處理,程式碼如下:
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<>();
if (isRollback) { // <1> 如果 isRollback 為 true ,返回空陣列
return Collections.emptyList();
}
// <2> 遍歷 statementList 和 batchResultList 陣列,逐個提交批處理
for (int i = 0, n = statementList.size(); i < n; i++) {
// <2.1> 獲得 Statement 和 BatchResult 物件
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
// <2.2> 提交該 Statement 的批處理
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
/*
* <2.3> 獲得 KeyGenerator 物件
* 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件
* 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件
* 否則為 NoKeyGenerator 物件
*/
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
// <2.3.1> 批處理入參物件集合,設定自增鍵
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { // issue #141
for (Object parameter : parameterObjects) {
// <2.3.1> 一次處理每個入參物件,設定自增鍵
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
// Close statement to close cursor #1109
// <2.4> 關閉 Statement 物件
closeStatement(stmt);
} catch (BatchUpdateException e) {
// 如果發生異常,則丟擲 BatchExecutorException 異常
StringBuilder message = new StringBuilder();
message.append(batchResult.getMappedStatement().getId())
.append(" (batch index #")
.append(i + 1)
.append(")")
.append(" failed.");
if (i > 0) {
message.append(" ")
.append(i)
.append(" prior sub executor(s) completed successfully, but will be rolled back.");
}
throw new BatchExecutorException(message.toString(), e, results, batchResult);
}
// <2.5> 新增到結果集
results.add(batchResult);
}
return results;
} finally {
// <3.1> 關閉 Statement 們
for (Statement stmt : statementList) {
closeStatement(stmt);
}
// <3.2> 置空 currentSql、statementList、batchResultList 屬性
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
在呼叫doUpdate方法將資料庫更新操作新增至批處理後,我們需要呼叫doFlushStatements方法執行批處理,邏輯如下:
-
如果
isRollback
為true
,表示需要回退,返回空陣列 -
遍歷
statementList
和batchResultList
陣列,逐個提交批處理- 獲得
Statement
和BatchResult
物件 - 提交該
Statement
的批處理 - 獲得
KeyGenerator
物件,用於設定自增鍵,在後續會講到? - 關閉
Statement
物件 - 將
BatchResult
物件新增到結果集
- 獲得
-
最後會關閉所有的
Statement
和清空當前會話中儲存的資料
二級快取
在BaseExecutor
中講到的一級快取中,快取資料僅在當前的 SqlSession 會話中進行共享,可能會導致多個 SqlSession 出現資料不一致性的問題
如果需要在多個 SqlSession 之間需要共享快取資料,則需要使用到二級快取
開啟二級快取
後,會使用CachingExecutor
物件裝飾其他的Executor
類,這樣會先在CachingExecutor
進行二級快取
的查詢,快取未命中則進入裝飾的物件中,進行一級快取
的查詢
流程如下圖所示:
在《MyBatis初始化》的一系列文件中講過MappedStatement會有一個Cache物件,是根據@CacheNamespace
註解或<cache />
標籤建立的物件,該物件也會儲存在Configuration
全域性配置物件的Map<String, Cache> caches = new StrictMap<>("Caches collection")
中,key為所在的namespace,也可以通過@CacheNamespaceRef
註解或<cache-ref />
標籤來指定其他namespace的Cache物件
在全域性配置物件中cacheEnabled
是否開啟快取屬性預設為true
,可以在mybatis-config.xml
配置檔案中新增以下配置關閉:
<configuration>
<settings>
<setting name="cacheEnabled" value="false" />
</settings>
</configuration>
我們來看看MyBatis是如何實現二級快取的
CachingExecutor
org.apache.ibatis.executor.CachingExecutor
:實現 Executor 介面,支援二級快取的 Executor 的實現類
構造方法
public class CachingExecutor implements Executor {
/**
* 被委託的 Executor 物件
*/
private final Executor delegate;
/**
* TransactionalCacheManager 物件
*/
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
// 設定 delegate 被當前執行器所包裝
delegate.setExecutorWrapper(this);
}
}
delegate
屬性,為被委託的Executor
物件,具體的資料庫操作都是交由它去執行tcm
屬性,TransactionalCacheManager
物件,支援事務的快取管理器,因為二級快取是支援跨 SqlSession 共享的,此處需要考慮事務,那麼,必然需要做到事務提交時,才將當前事務中查詢時產生的快取,同步到二級快取中,所以需要通過TransactionalCacheManager
來實現
query方法
處理資料庫查詢操作的方法,涉及到二級快取,會將Cache
二級快取物件裝飾成TransactionalCache
物件並存放在TransactionalCacheManager
管理器中,程式碼如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// <1> 獲取 Cache 二級快取物件
Cache cache = ms.getCache();
// <2> 如果配置了二級快取
if (cache != null) {
// <2.1> 如果需要清空快取,則進行清空
flushCacheIfRequired(ms);
// <2.2> 如果當前操作需要使用快取(預設開啟)
if (ms.isUseCache() && resultHandler == null) {
// <2.2.1> 如果是儲存過程相關操作,保證所有的引數模式為 ParameterMode.IN
ensureNoOutParams(ms, boundSql);
// <2.2.2> 從二級快取中獲取結果,會裝飾成 TransactionalCache
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// <2.2.3> 如果不存在,則從資料庫中查詢
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// <2.2.4> 將快取結果儲存至 TransactionalCache
tcm.putObject(cache, key, list); // issue #578 and #116
}
// <2.2.5> 直接返回結果
return list;
}
}
// <3> 沒有使用二級快取,則呼叫委託物件的方法
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
-
獲取
Cache
二級快取物件 -
如果該物件不為空,表示配置了二級快取
- 如果需要清空快取,則進行清空
- 如果當前操作需要使用快取(預設開啟)
- 如果是儲存過程相關操作,保證所有的引數模式為
ParameterMode.IN
- 通過
TransactionalCacheManager
從二級快取中獲取結果,會裝飾成TransactionalCach
物件 - 如果快取未命中,則呼叫委託物件的
query
方法 - 將快取結果儲存至
TransactionalCache
物件中,並未真正的儲存至Cache
二級快取中,需要待事務提交才會儲存過去,其中快取未命中的也會設定快取結果為null - 直接返回結果
- 如果是儲存過程相關操作,保證所有的引數模式為
-
沒有使用二級快取,則呼叫委託物件的方法
update方法
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 如果需要清空快取,則進行清空
flushCacheIfRequired(ms);
// 執行 delegate 對應的方法
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
資料庫的更新操作,如果配置了需要清空快取,則清空二級快取
這裡就和一級快取不同,一級快取是所有的更新操作都會清空一級快取
commit方法
@Override
public void commit(boolean required) throws SQLException {
// 執行 delegate 對應的方法
delegate.commit(required);
// 提交 TransactionalCacheManager
tcm.commit();
}
在事務提交後,通過TransactionalCacheManager
二級快取管理器,將本次事務生成的快取資料從TransactionalCach
中設定到正真的Cache
二級快取中
rollback方法
@Override
public void rollback(boolean required) throws SQLException {
try {
// 執行 delegate 對應的方法
delegate.rollback(required);
} finally {
if (required) {
// 回滾 TransactionalCacheManager
tcm.rollback();
}
}
}
在事務回滾後,如果需要的話,通過TransactionalCacheManager
二級快取管理器,將本次事務生成的快取資料從TransactionalCach
中移除
close方法
@Override
public void close(boolean forceRollback) {
try {
// issues #499, #524 and #573
if (forceRollback) {
tcm.rollback();
} else {
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
在事務關閉前,如果是強制回滾操作,則TransactionalCacheManager
二級快取管理器,將本次事務生成的快取資料從TransactionalCach
中移除,否則還是將快取資料設定到正真的Cache
二級快取中
TransactionalCacheManager
org.apache.ibatis.cache.TransactionalCacheManager
:二級快取管理器,因為二級快取是支援跨 SqlSession 共享的,所以需要通過它來實現,當事務提交時,才將當前事務中查詢時產生的快取,同步到二級快取中,程式碼如下:
public class TransactionalCacheManager {
/**
* Cache 和 TransactionalCache 的對映
*/
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
// 首先,獲得 Cache 對應的 TransactionalCache 物件
// 然後,新增 KV 到 TransactionalCache 物件中
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
-
getTransactionalCache(Cache cache)
方法,根據Cache
二級快取物件獲取對應的TransactionalCache
物件,如果沒有則建立一個儲存起來 -
getObject(Cache cache, CacheKey key)
方法,會先呼叫getTransactionalCache(Cache cache)
方法獲取對應的TransactionalCache
物件,然後根據CacheKey
從該物件中獲取快取結果 -
putObject(Cache cache, CacheKey key, Object value)
方法,同樣也先呼叫getTransactionalCache(Cache cache)
方法獲取對應的TransactionalCache
物件,根據該物件將結果進行快取 -
commit()
方法,遍歷transactionalCaches
,依次呼叫TransactionalCache
的提交方法 -
rollback()
方法,遍歷transactionalCaches
,依次呼叫TransactionalCache
的回滾方法
TransactionalCache
org.apache.ibatis.cache.decorators.TransactionalCache
:用來裝飾二級快取的物件,作為二級快取一個事務的緩衝區
在一個SqlSession會話中,該類包含所有需要新增至二級快取的的快取資料,當提交事務後會全部刷出到二級快取中,或者事務回滾後移除這些快取資料,程式碼如下:
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
/**
* 委託的 Cache 物件。
*
* 實際上,就是二級快取 Cache 物件。
*/
private final Cache delegate;
/**
* 提交時,清空 {@link #delegate}
*
* 初始時,該值為 false
* 清理後{@link #clear()} 時,該值為 true ,表示持續處於清空狀態
*
* 因為可能事務還未提交,所以不能直接清空所有的快取,而是設定一個標記,獲取快取的時候返回 null 即可
* 先清空下面這個待提交變數,待事務提交的時候才真正的清空快取
*
*/
private boolean clearOnCommit;
/**
* 待提交的 Key-Value 對映
*/
private final Map<Object, Object> entriesToAddOnCommit;
/**
* 查詢不到的 KEY 集合
*/
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public Object getObject(Object key) {
// issue #116
// <1> 從 delegate 中獲取 key 對應的 value
Object object = delegate.getObject(key);
if (object == null) {// <2> 如果不存在,則新增到 entriesMissedInCache 中
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {// <3> 如果 clearOnCommit 為 true ,表示處於持續清空狀態,則返回 null
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
// 暫存 KV 到 entriesToAddOnCommit 中
entriesToAddOnCommit.put(key, object);
}
@Override
public void clear() {
// <1> 標記 clearOnCommit 為 true
clearOnCommit = true;
// <2> 清空 entriesToAddOnCommit
entriesToAddOnCommit.clear();
}
public void commit() {
// <1> 如果 clearOnCommit 為 true ,則清空 delegate 快取
if (clearOnCommit) {
delegate.clear();
}
// 將 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
flushPendingEntries();
// 重置
reset();
}
public void rollback() {
// <1> 從 delegate 移除出 entriesMissedInCache
unlockMissedEntries();
// <2> 重置
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
根據上面的註釋檢視每個屬性的作用,我們依次來看下面的方法,看看在不同事務之前是如何處理二級快取的
-
putObject(Object key, Object object)
方法,新增快取資料時,先把快取資料儲存在entriesToAddOnCommit
中,這個物件屬於當前事務,事務還未提交,其他事務是不能訪問到的 -
clear()
方法,設定clearOnCommit
標記為true,告訴當前事務正處於持續清空狀態,先把entriesToAddOnCommit
清空,也就是當前事務中還未提交至二級快取的快取資料,事務還未提交,不能直接清空二級快取中的資料,否則影響到其他事務了 -
commit()
方法,事務提交後,如果clearOnCommit
為true,表示正處於持續清空狀態,需要先把二級快取中的資料全部清空,然後再把當前事務生成的快取設定到二級快取中,然後重置當前物件這裡為什麼處於清空狀態把二級快取的資料清空後,還要將當前事務生成的快取資料再設定到二級快取中呢?因為當前事務呼叫
clear()
方法後可能有新生成了新的快取資料,而不能把這些忽略掉 -
getObject(Object key)
方法- 先從
delegate
二級快取物件中獲取結果 - 如果快取未命中則將該key新增到
entriesMissedInCache
屬性中,因為二級快取也會將快取未命中的key起來,資料為null - 如果
clearOnCommit
為true,即使你快取命中了也返回null,因為觸發clear()
方法的話,本來需要清空二級快取的,但是事務還未提交,所以先標記一個快取持續清理的這麼一個狀態,這樣相當於在當前事務中既清空了二級快取資料,也不影響其他事務的二級快取資料 - 返回獲取到的結果,可能為null
- 先從
Executor在哪被建立
前面對Executor執行器介面以及實現類都有分析過,那麼它是在哪建立的呢?
在《MyBatis初始化(一)之載入mybatis-config.xml》這一篇文件中講到,整個的初始化入口在SqlSessionFactoryBuilder
的build
方法中,建立的是一個DefaultSqlSessionFactory
物件,該物件用來建立SqlSession
會話的,我們來瞧一瞧:
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
boolean autoCommit) {
Transaction tx = null;
try {
// 獲得 Environment 物件
final Environment environment = configuration.getEnvironment();
// 建立 Transaction 物件
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 建立 Executor 物件
final Executor executor = configuration.newExecutor(tx, execType);
// 建立 DefaultSqlSession 物件
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
// 如果發生異常,則關閉 Transaction 物件
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
}
我們所有的資料庫操作都是在MyBatis的一個SqlSession
會話中執行的,在它被建立的時候,會先通過Configuration
全域性配置物件的newExecutor
方法建立一個Executor
執行器
newExecutor(Transaction transaction, ExecutorType executorType)
方法,根據執行器型別建立執行Executor
執行器,程式碼如下:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// <1> 獲得執行器型別
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
// <2> 建立對應實現的 Executor 物件
Executor executor;
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);
}
// <3> 如果開啟快取,建立 CachingExecutor 物件,進行包裝
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// <4> 應用外掛
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
- 獲得執行器型別,預設為
SIMPLE
- 建立對應的
Executor
物件,預設就是SimpleExecutor
執行器了 - 如果全域性配置了開啟二級快取,則將
Executor
物件,封裝成CachingExecutor
物件 - 外掛鏈應用該物件,在後續會講到?
總結
本文分析了MyBatis在執行SQL的過程中,都是在SimpleExecutor
(預設型別)執行器中進行的,由它呼叫其他“元件”來完成資料庫操作
其中需要通過PrepareStatementHandler
(預設)來建立對應的PrepareStatemen
,進行引數的設定等相關處理,執行資料庫操作
獲取到結果後還需要通過DefaultResultSetHandler
進行引數對映,轉換成對應的Java物件,這兩者在後續會進行分析
關於MyBatis的快取,存在侷限性,我們通常不會使用,如有需要使用快取,檢視我的另一篇原始碼解析文件《JetCache原始碼分析》
一級快取
僅限於單個 SqlSession 會話,多個 SqlSession 可能導致資料的不一致性,例如某一個 SqlSession 更新了資料而其他 SqlSession 無法獲取到更新後的資料,出現資料不一致性,這種情況是不允許出現了
二級快取
MyBatis配置
二級快取
是通過在XML對映檔案新增<cache / >
標籤建立的(註解也可以),所以不同的XML對映檔案所對應的二級快取
物件可能不是同一個
二級快取
雖然解決的一級快取
中存在的多個 SqlSession 會話可能出現髒讀的問題,但還是針對同一個二級快取物件不會出現這種情況,如果其他的XML對映檔案修改了相應的資料,當前二級快取
獲取到的快取資料就不是最新的資料,也出現了髒讀的問題例如,在一個XML對映檔案中配置了二級快取,獲取到某個使用者的資訊並存放在對應的二級快取物件中,其他的XML對映檔案修改了這個使用者的資訊,那麼之前那個快取資料就不是最新的
當然你可以XML對映檔案對指向同一個Cache物件(通過
<cache-ref / >
標籤),這樣就太侷限了,所以MyBatis的快取存在一定的缺陷,且快取的資料僅僅是儲存在了本地記憶體中,對於當前高併發的環境下是無法滿足要求的,所以我們通常不使用MyBatis的快取
參考文章:芋道原始碼《精盡 MyBatis 原始碼分析》