該系列檔案是本人在學習 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 = https://www.cnblogs.com/lifullmoon/p/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 = https://www.cnblogs.com/lifullmoon/p/Integer.MIN_VALUE + 1002;
/**
* Statement 陣列
*/
private final List statementList = new ArrayList<>();
/**
* BatchResult 陣列
*
* 每一個 BatchResult 元素,對應 {@link #statementList} 集合中的一個 Statement 元素
*/
private final List 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="https://www.cnblogs.com/lifullmoon/p/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 原始碼分析》
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/227226.html
標籤:Java
上一篇:Java基本概念:類
