在瞭解了MyBatis初始化載入過程後,我們也應該研究看看SQL執行過程是怎樣執行?這樣我們對於Mybatis的整個執行流程都熟悉了,在開發遇到問題也可以很快定位到問題。
更重要的,在面試中遇到面試官諮詢Mybatis的知識點的時候,可以很順暢的把這一套流程講出來,面試官也會覺得你已掌握Mybatis知識點了,可能就不問了。趕緊瞄瞄。
經過MyBatis初始化載入Sql執行過程所需的資訊後,我們就可以通過 SqlSessionFactory
物件得到 SqlSession
,然後執行 SQL 語句了,接下來看看Sql執行具體過程,SQL大致執行流程圖如下所示:
接下來我們來看看每個執行鏈路中的具體執行過程,
SqlSession 是 MyBatis 暴露給外部使用的統一介面層,通過 SqlSessionFactory
建立,且其是包含和資料庫打交道所有操作介面。
下面通過時序圖描述 SqlSession 物件的建立流程:
在生成SqlSession
的同時,基於executorType
初始化好Executor
實現類。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
最頂層的SqlSession介面已生成,那我們可以來看看sql的執行過程下一步是怎樣的呢?怎樣使用代理類MapperProxy
。
MapperProxy
是 Mapper
介面與SQL 語句對映的關鍵,通過 MapperProxy
可以讓對應的 SQL 語句跟介面進行繫結的,具體流程如下:
MapperProxy
代理類生成流程MapperProxy
代理類執行操作MapperProxy
代理類生成流程
其中,MapperRegistry
是 Configuration
的一個屬性,在解析設定時候會在MapperRegistry
中快取了 MapperProxyFactory
的 knownMappers
變數Map
集合。
``MapperRegistry會根據
mapper介面型別獲取已快取的
MapperProxyFactory,
MapperProxyFactory會基於
SqlSession來生成
MapperProxy`代理物件,
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
當呼叫SqlSession
介面時,MapperProxy
怎麼是實現的呢?MyBatis
的 Mapper
介面 是通過動態代理實現的,呼叫 Mapper
介面的任何方法都會執行 MapperProxy::invoke()
方法,
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//Object型別執行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
//介面預設方法執行
if (method.isDefault()) {
if (privateLookupInMethod == null) {
return this.invokeDefaultMethodJava8(proxy, method, args);
}
return this.invokeDefaultMethodJava9(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
MapperMethod mapperMethod = this.cachedMapperMethod(method);
return mapperMethod.execute(this.sqlSession, args);
}
但最終會呼叫到mapperMethod::execute()
方法執行,主要是判斷是 INSERT
、UPDATE
、DELETE
、SELECT
語句去操作,其中如果是查詢的話,還會判斷返回值的型別。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
通過以上的分析,總結出
Mapper
介面實際物件為代理物件MapperProxy
;MapperProxy
繼承InvocationHandler
,實現invoke
方法;MapperProxyFactory::newInstance()
方法,基於 JDK 動態代理的方式建立了一個 MapperProxy
的代理類;mapperMethod::execute()
方法執行,完成操作。MyBatis
使用的動態代理和普遍動態代理有點區別,沒有實現類,只有介面,MyBatis 動態代理類圖結構如下所示:已以SELECT
為例, 呼叫會SqlSession ::selectOne()
方法。繼續往下執行,會執行 Executor::query()
方法。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
MappedStatement ms = this.configuration.getMappedStatement(statement);
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}
return var5;
}
執行到Executor
類,那麼我們來看看其究竟有什麼?
Executor
物件為SQL 的執行引擎,負責增刪改查的具體操作,頂層介面SqlSession
中都會有一個 Executor
物件,可以理解為 JDBC 中 Statement
的封裝版。
Executor
是最頂層的是執行器,它有兩個實現類,分別是BaseExecutor
和 CachingExecutor
BaseExecutor
是一個抽象類,實現了大部分 Executor
介面定義的功能,降低了介面實現的難度。BaseExecutor
基於介面卡設計模式之介面適配會有三個子類,分別是 SimpleExecutor
、ReuseExecutor
和 BatchExecutor
。
SimpleExecutor
: 是 MyBatis 中預設簡單執行器,每執行一次update
或select
,就開啟一個Statement
物件,用完立刻關閉Statement
物件
ReuseExecutor
: 可重用執行器, 執行update
或select
,以sql作為key
查詢Statement
物件,存在就使用,不存在就建立,用完後,不關閉Statement
物件,而是放置於Map<String, Statement>
內,供下一次使用。簡言之,就是重複使用Statement
物件
BatchExecutor
: 批次處理執行器,用於執行update(沒有select,JDBC批次處理不支援select將多個 SQL 一次性輸出到資料庫,
CachingExecutor
: 快取執行器,為Executor
物件增加了二級快取的相關功:先從快取中查詢結果,如果存在就返回之前的結果;如果不存在,再委託給Executor delegate
去資料庫中取,delegate
可以是上面任何一個執行器。
在Mybatis
組態檔中,可以指定預設的ExecutorType
執行器型別,也可以手動給DefaultSqlSessionFactory
的建立SqlSession
的方法傳遞ExecutorType
型別引數。
看完Exector
簡介之後,繼續跟蹤執行流程鏈路分析,SqlSession
中的 JDBC
操作部分最終都會委派給 Exector
實現,Executor::query()
方法,看看在Exector
的執行是怎樣的?
每次查詢都會先經過CachingExecutor
快取執行器, 會先判斷二級快取中是否存在查詢 SQL ,如果存在直接從二級快取中獲取,不存在即為第一次執行,會直接執行SQL 語句,並建立快取,都是由CachingExecutor::query()
操作完成的。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//獲取查詢語句對應的二級快取
Cache cache = ms.getCache();
//sql查詢是否存在在二級快取中
if (cache != null) {
//根據 <select> 節點的設定,判斷否需要清空二級快取
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
//查詢二級快取
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
//二級快取沒用相應的結果物件,呼叫封裝的Executor物件的 query() 方法
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//將查詢結果儲存到二級快取中
this.tcm.putObject(cache, key, list);
}
return list;
}
}
//沒有啟動二級快取,直接呼叫底層 Executor 執行資料資料庫查詢操作
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果在經過CachingExecutor
快取執行器(二級快取)沒有返回值的話,就會執行BaseExecutor
以及其的實現類,預設為SimpleExecutor
,首先會在一級快取中獲取查詢結果,獲得不到,最終會通過SimpleExecutor:: ()
去資料庫中查詢。
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 (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
//是否清除本地快取
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
List list;
try {
++this.queryStack;
//從一級快取中,獲取查詢結果
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
//獲取到結果,則進行處理
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//獲得不到,則從資料庫中查詢
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
--this.queryStack;
}
if (this.queryStack == 0) {
//執行延遲載入
Iterator var8 = this.deferredLoads.iterator();
while(var8.hasNext()) {
BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();
}
this.deferredLoads.clear();
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
}
return list;
}
}
那麼SimpleExecutor::doQuery()
如何去資料庫中查詢獲取到結果呢?其實執行到這邊mybatis的執行過程就從 Executor
轉交給 StatementHandler
處理,
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
List var9;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var9 = handler.query(stmt, resultHandler);
} finally {
this.closeStatement(stmt);
}
return var9;
}
這樣我們的執行鏈路分析已到StatementHandler
了,現在讓我們去一探究竟其原理
StatementHandler
負責處理Mybatis
與JDBC之間Statement
的互動,即Statement
物件與資料庫進行互動,其為頂級介面,有4個實現類,其中三個是Statement
物件與資料庫進行互動類, 另外一個是路由功能的,
RoutingStatementHandler
: 對 Statement
物件沒有實際操作,主要負責另外三個StatementHandler
的建立及呼叫, 而且在MyBatis
執行時,使用的StatementHandler
介面物件實際上就是 RoutingStatementHandler
物件。SimpleStatementHandler
: 管理 Statement 物件, 用於簡單SQL的處理 。PreparedStatementHandler
: 管理 Statement 物件,預處理SQL的介面 。CallableStatementHandler
:管理 Statement 物件,用於執行儲存過程相關的介面 。在經歷過Executor
後,基於初始化載入到MapperState
中的StatementType
的型別通過Configuration.newStatementHandler()
方法中的RoutingStatementHandler
生成StatementHandler
實際處理類。
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch(ms.getStatementType()) {
case STATEMENT:
this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
現在先以PreparedStatementHandler
預處理為例,接著Sql的執行鏈路來分析,StatementHandler::query()
到StatementHandler::execute()
真正執行Sql查詢操作。
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.handleResultSets(statement);
}
但執行真正查詢操作之前,還進行哪些處理呢?還會進行ParameterHandler
對 SQL 引數的預處理:對引數進行動態Sql對映,那麼ParameterHandler
又如何實現對引數進行動態對映的呢?
ParameterHandler
引數處理器, 用來設定引數規則的,負責為sql 語句引數動態賦值,其有兩個介面
當SimpleExecutor
執行構造PreparedStatementHandler
完,會呼叫parameterize()
方法將PreparedStatement
物件裡SQL轉交ParameterHandler
實現類 DefaultParameterHandler::setParameters()
方法 設定 PreparedStatement
的預留位置引數 。
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;
}
DefaultParameterHandler::setParameters()
如何對SQL進行動態賦值呢?在執行前將已裝載好的BoundSql物件資訊進行使用
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//獲取待動態賦值參數列的封裝parameterMappings
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
//是否為輸入引數
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
//獲取待動態引數屬性名
String propertyName = parameterMapping.getProperty();
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;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
在通過 SqlSource 的parse 方法得到parameterMappings 的具體實現中,我們會得到parameterMappings 的 typeHandler
TypeHandler typeHandler = parameterMapping.getTypeHandler();
//獲取jdbc資料型別
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
執行完SQL 引數的預處理,當StatementHandler::execute()
真正執行查詢操作執行完後,有返回結果,需要對返回結果進行ResultSetHandler
處理,現在看看最後的結果的處理流程。
ResultSetHandler
結果解析器,將查詢結果的ResultSet
轉換成對映的對應結果(java DTO
等),其有三介面
handleResultSets()
:處理結果集handleCursorResultSets()
:批次處理結果集handleOutputParameters()
:處理儲存過程返回的結果集其預設的實現為DefaultResultSetHandler
,主要功能為:
Statement
執行後產生的結果集生成相對的輸出結果、那看看DefaultResultSetHandler::handleResultSets()
如何處理?
ResultSet
的結果集合,每個ResultSet對應一個Object 物件,如果不考慮儲存過程,普通的查詢只有一個ResultSetResultSetWrapper
封裝了ResultSet
結果集,其屬性包含ResultSet
,ResultMap
等@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
//當有多個ResultSet的結果集合,每個ResultSet對應一個Object 物件
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
//獲得首個 ResultSet 物件,並封裝成 ResultSetWrapper 物件
ResultSetWrapper rsw = getFirstResultSet(stmt);
//獲得 ResultMap 陣列
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount); // <3.1> 校驗
while (rsw != null && resultMapCount > resultSetCount) {
//獲得 ResultMap 物件
ResultMap resultMap = resultMaps.get(resultSetCount);
//處理 ResultSet ,將結果新增到 multipleResults 中
handleResultSet(rsw, resultMap, multipleResults, null);
//獲得下一個 ResultSet 物件,並封裝成 ResultSetWrapper 物件
rsw = getNextResultSet(stmt);
//清理
cleanUpAfterHandlingResultSet();
// resultSetCount ++
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++;
}
}
//如果是 multipleResults 單元素,則取首元素返回
return ollapseSingleResultList(multipleResults);
}
其實在ResultSetHandler
結果集處理是比較複雜的,這裡只是簡單的介紹一下,有興趣的可以再深入研究一下,後期有空也會寫。
執行到這邊,Mybatis SQL執行基本完了,會把轉換後的結果集返回到操作者。
在SQL執行過程主要涉及了SqlSession
,MapperProxy
,Executor
,StatementHandler
,ParameterHandler
以及ResultSetHandler
,包括引數動態繫結,Sql執行查詢資料庫資料,結果返回集對映等,而且每個環節涉及的內容都很多,每個介面都可以抽出單獨分析,後續有時間再一一詳細的看看。後面還是再分析一下外掛的應用。
各位看官還可以嗎?喜歡的話,動動手指點個💗,點個關注唄!!謝謝支援!
歡迎關注,原創技術文章第一時間推出