詳解MyBatis中Executor執(zhí)行SQL語句的過程
前言
在詳解MyBatis的SqlSession獲取流程文章中已經(jīng)知道,MyBatis中獲取SqlSession時(shí)會(huì)創(chuàng)建執(zhí)行器Executor并存放在SqlSession中,通過SqlSession可以獲取映射接口的動(dòng)態(tài)代理對(duì)象,可以用下圖進(jìn)行概括
所以,映射接口的動(dòng)態(tài)代理對(duì)象實(shí)際執(zhí)行方法時(shí),執(zhí)行的請(qǐng)求最終會(huì)由MapperMethod的execute() 方法完成。從MapperMethod的execute() 方法開始,后續(xù)執(zhí)行流程,可以用下圖進(jìn)行示意。
本篇文章將以MapperMethod的execute() 方法作為起點(diǎn),對(duì)MyBatis中的一次實(shí)際執(zhí)行請(qǐng)求進(jìn)行說明,并結(jié)合源碼對(duì)執(zhí)行器Executor的原理進(jìn)行闡釋。
本篇文章不會(huì)對(duì)MyBatis中的緩存進(jìn)行說明,關(guān)于MyBatis中的一級(jí)緩存和二級(jí)緩存相關(guān)內(nèi)容,會(huì)在后續(xù)的文章中單獨(dú)進(jìn)行分析,為了屏蔽MyBatis中的二級(jí)緩存的干擾,需要在MyBatis的配置文件中添加如下配置以禁用二級(jí)緩存。
<settings> <setting name="cacheEnabled" value="false"/> </settings>
MyBatis版本:3.5.6
正文
本節(jié)將以一個(gè)實(shí)際的查詢例子,以單步跟蹤并結(jié)合源碼的方法,對(duì)MyBatis的一次實(shí)際執(zhí)行請(qǐng)求進(jìn)行說明。給定映射接口如下所示。
public interface BookMapper { Book selectBookById(int id); }
給定映射文件如下所示。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.mybatis.learn.dao.BookMapper"> <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book"> <result property="bookName" column="b_name"/> <result property="bookPrice" column="b_price"/> </resultMap> <select id="selectBookById" resultMap="bookResultMap"> SELECT b.id, b.b_name, b.b_price FROM book b WHERE b.id=#{id} </select> </mapper>
MyBatis的執(zhí)行代碼如下所示。
public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); // 獲取SqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // 獲取映射接口的動(dòng)態(tài)代理對(duì)象 BookMapper bookMapper = sqlSession.getMapper(BookMapper.class); // 執(zhí)行一次查詢操作 System.out.println(bookMapper.selectBookById(1)); } }
基于上述的映射接口,映射文件和執(zhí)行代碼,最終執(zhí)行查詢操作時(shí),會(huì)調(diào)用到MapperMethod的execute() 方法并進(jìn)入查詢的邏輯分支,這部分源碼如下所示。
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { // ...... case SELECT: // 根據(jù)實(shí)際執(zhí)行的方法的返回值的情況進(jìn)入不同的邏輯分支 if (method.returnsVoid() && method.hasResultHandler()) { // 無返回值情況 executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { // 返回值為集合的情況 result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { // 返回值為map的情況 result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { // 返回值為迭代器的情況 result = executeForCursor(sqlSession, args); } else { // 上述情況之外的情況 // 將方法的入?yún)⑥D(zhuǎn)換為Sql語句的參數(shù) Object param = method.convertArgsToSqlCommandParam(args); // 調(diào)用DefaultSqlSession的selectOne()方法執(zhí)行查詢操作 result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; // ...... } // ...... return result; }
已知映射接口中的每個(gè)方法都會(huì)對(duì)應(yīng)一個(gè)MapperMethod,MapperMethod中的SqlCommand會(huì)指示該方法對(duì)應(yīng)的MappedStatement信息和類型信息(SELECT,UPDATE等),MapperMethod中的MethodSignature會(huì)存儲(chǔ)該方法的參數(shù)信息和返回值信息,所以在上述的MapperMethod的execute() 方法中,首先根據(jù)SqlCommand的指示的類型進(jìn)入不同的邏輯分支,本示例中會(huì)進(jìn)入SELECT的邏輯分支,然后又會(huì)根據(jù)MethodSignature中指示的方法返回值情況進(jìn)入不同的查詢分支,本示例中的方法返回值既不是集合,map或迭代器,也不是空,所以會(huì)進(jìn)入查詢一條數(shù)據(jù)的查詢分支。
在MapperMethod中的execute() 方法中會(huì)調(diào)用到DefaultSqlSession的selectOne() 方法執(zhí)行查詢操作,該方法實(shí)現(xiàn)如下所示。
@Override public <T> T selectOne(String statement) { return this.selectOne(statement, null); } @Override public <T> T selectOne(String statement, Object parameter) { // 查詢操作會(huì)由selectList()完成 List<T> list = this.selectList(statement, parameter); if (list.size() == 1) { // 查詢結(jié)果只有一個(gè)時(shí),返回查詢結(jié)果 return list.get(0); } else if (list.size() > 1) { // 查詢結(jié)果大于一個(gè)時(shí),報(bào)錯(cuò) throw new TooManyResultsException( "Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else { return null; } }
DefaultSqlSession的selectOne() 方法中會(huì)將查詢請(qǐng)求交由DefaultSqlSession的selectList() 方法完成,如果selectList() 方法返回的結(jié)果集合中只有一個(gè)返回值,就將這個(gè)返回值返回,如果多于一個(gè)返回值,就報(bào)錯(cuò)。DefaultSqlSession的selectList() 方法如下所示。
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { // 從Configuration中的mappedStatements緩存中獲取MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); // 調(diào)用Executor的query()方法執(zhí)行查詢操作 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
在DefaultSqlSession的selectList() 方法中,會(huì)先根據(jù)statement參數(shù)值在Configuration中的mappedStatements緩存中獲取MappedStatement,statement參數(shù)值其實(shí)就是MapperMethod中的SqlCommand的name字段,是MappedStatement在mappedStatements緩存中的唯一標(biāo)識(shí)。獲取到MappedStatement后,就會(huì)調(diào)用Executor的query() 方法執(zhí)行查詢操作,因?yàn)榻昧硕?jí)緩存,所以這里的Executor實(shí)際上為SimpleExecutor。本示例中單步跟蹤到這里時(shí),數(shù)據(jù)如下所示。
SimpleExecutor的類圖如下所示。
SimpleExecutor和BaseExecutor之間使用了模板設(shè)計(jì)模式,調(diào)用SimpleExecutor的query() 方法時(shí)會(huì)調(diào)用到BaseExecutor的query() 方法,如下所示。
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 獲取Sql語句 BoundSql boundSql = ms.getBoundSql(parameter); // 生成CacheKey CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 調(diào)用重載的query()方法 return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
繼續(xù)看BaseExecutor中的重載的query() 方法,如下所示。
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; // 先從一級(jí)緩存中根據(jù)CacheKey命中查詢結(jié)果 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { // 成功命中,則返回緩存中的查詢結(jié)果 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 未命中,則直接查數(shù)據(jù)庫 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (BaseExecutor.DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); } } return list; }
上述的query() 方法大部分邏輯是在為MyBatis中的一級(jí)緩存服務(wù),這里暫時(shí)不分析,除開緩存的邏輯,上述query() 方法做的事情可以概括為:
- 先從緩存中獲取查詢結(jié)果;
- 獲取到則返回緩存中的查詢結(jié)果;
- 否則直接查詢數(shù)據(jù)庫。
下面分析直接查詢數(shù)據(jù)庫的邏輯,queryFromDatabase() 方法的實(shí)現(xiàn)如下所示。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 調(diào)用doQuery()進(jìn)行查詢操作 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } // 將查詢結(jié)果添加到一級(jí)緩存中 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } // 返回查詢結(jié)果 return list; }
上述queryFromDatabase() 方法中,會(huì)調(diào)用BaseExecutor定義的抽象方法doQuery() 進(jìn)行查詢,本示例中,doQuery() 方法由SimpleExecutor進(jìn)行了實(shí)現(xiàn),如下所示。
@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(); // 創(chuàng)建RoutingStatementHandler StatementHandler handler = configuration.newStatementHandler( wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 實(shí)例化Statement stmt = prepareStatement(handler, ms.getStatementLog()); // 執(zhí)行查詢 return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }
上述的doQuery() 方法中,做了三件事情:
第一件事情
是創(chuàng)建RoutingStatementHandler
;第二件事情
是實(shí)例化Statement
;第三件事情
是執(zhí)行查詢。
第一件事情
:創(chuàng)建RoutingStatementHandler
實(shí)際上RoutingStatementHandler正如其名字所示,僅僅只是做一個(gè)路由轉(zhuǎn)發(fā)的作用,在創(chuàng)建RoutingStatementHandler時(shí),會(huì)根據(jù)映射文件中CURD標(biāo)簽上的statementType屬性決定創(chuàng)建什么類型的StatementHandler并賦值給RoutingStatementHandler中的delegate字段,后續(xù)對(duì)RoutingStatementHandler的所有操作均會(huì)被其轉(zhuǎn)發(fā)給delegate。
此外在初始化SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler時(shí)還會(huì)一并初始化ParameterHandler和ResultSetHandler。
映射文件中CURD標(biāo)簽上的statementType屬性與StatementHandler的對(duì)應(yīng)關(guān)系如下。
statementType屬性 | 對(duì)應(yīng)的StatementHandler | 作用 |
---|---|---|
STATEMENT | SimpleStatementHandler | 直接操作SQL,不進(jìn)行預(yù)編譯 |
PREPARED | PreparedStatementHandler | 預(yù)編譯SQL |
CALLABLE | CallableStatementHandler | 執(zhí)行存儲(chǔ)過程 |
RoutingStatementHandler與SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler的關(guān)系可以用下圖示意。
在創(chuàng)建RoutingStatementHandler之后,還會(huì)為RoutingStatementHandler植入插件邏輯。Configuration的newStatementHandler() 方法實(shí)現(xiàn)如下。
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 創(chuàng)建RoutingStatementHandler StatementHandler statementHandler = new RoutingStatementHandler( executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 為RoutingStatementHandler植入插件邏輯 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
第二件事情
:實(shí)例化Statement
prepareStatement() 方法實(shí)現(xiàn)如下所示。
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; // 獲取到Connection對(duì)象并為Connection對(duì)象生成動(dòng)態(tài)代理對(duì)象 Connection connection = getConnection(statementLog); // 通過Connection對(duì)象的動(dòng)態(tài)代理對(duì)象實(shí)例化Statement stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }
prepareStatement() 方法中首先會(huì)從Transaction中將數(shù)據(jù)庫連接對(duì)象Connection對(duì)象獲取出來并為其生成動(dòng)態(tài)代理對(duì)象以實(shí)現(xiàn)日志打印功能的增強(qiáng),然后會(huì)通過Connection的動(dòng)態(tài)代理對(duì)象實(shí)例化Statement,最后會(huì)處理Statement中的占位符,比如將PreparedStatement中的?
替換為實(shí)際的參數(shù)值。
第三件事情
:執(zhí)行查詢
本篇文章的示例中,映射文件的CURD標(biāo)簽沒有對(duì)statementType屬性進(jìn)行設(shè)置,因此查詢的操作最終會(huì)被RoutingStatementHandler路由轉(zhuǎn)發(fā)給PreparedStatementHandler的query() 方法,如下所示。
@Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // 調(diào)用到JDBC的邏輯了 ps.execute(); // 調(diào)用ResultSetHandler處理查詢結(jié)果 return resultSetHandler.handleResultSets(ps); }
如上所示,在PreparedStatementHandler的query() 方法中就會(huì)調(diào)用到JDBC的邏輯向數(shù)據(jù)庫進(jìn)行查詢,最后還會(huì)使用已經(jīng)初始化好并植入了插件邏輯的ResultSetHandler處理查詢結(jié)果并返回。
至此,對(duì)MyBatis的一次實(shí)際執(zhí)行請(qǐng)求的說明到此為止,本篇文章中的示例以查詢?yōu)槔鰟h改大體類似,故不再贅述。
總結(jié)
MyBatis中的執(zhí)行器Executor會(huì)在創(chuàng)建SqlSession時(shí)一并被創(chuàng)建出來并被存放于SqlSession中,如果禁用了二級(jí)緩存,則Executor實(shí)際為SimpleExecutor,否則為CachingExecutor。
MyBatis中的一次實(shí)際執(zhí)行,會(huì)由所執(zhí)行方法對(duì)應(yīng)的MapperMethod的execute() 方法完成。在execute() 方法中,會(huì)根據(jù)執(zhí)行操作的類型(增改刪查)調(diào)用SqlSession中的相應(yīng)的方法,例如insert(),update(),delete() 和select() 等。MapperMethod在這其中的作用就是MapperMethod關(guān)聯(lián)著本次執(zhí)行方法所對(duì)應(yīng)的SQL語句以及入?yún)⒑统鰠⒌刃畔ⅰ?/p>
在SqlSession的insert(),update(),delete() 和select() 等方法中,SqlSession會(huì)將與數(shù)據(jù)庫的操作交由執(zhí)行器Executor來完成。無論是在SimpleExecutor還是CachingExecutor中,如果拋開緩存相關(guān)的邏輯,這些Executor均會(huì)先根據(jù)映射文件中CURD標(biāo)簽的statementType字段創(chuàng)建相應(yīng)的StatementHandler,創(chuàng)建StatementHandler的過程中還會(huì)一并將處理參數(shù)和處理結(jié)果的ParameterHandler和ResultSetHandler創(chuàng)建出來,創(chuàng)建好StatementHandler之后,會(huì)基于StatementHandler實(shí)例化Statement,最后在StatementHandler中基于實(shí)例化好的Statement完成和數(shù)據(jù)庫的交互,基于創(chuàng)建好的ResultSetHandler處理交互結(jié)果并將結(jié)果返回。
以上就是詳解MyBatis中Executor執(zhí)行SQL語句的過程的詳細(xì)內(nèi)容,更多關(guān)于MyBatis Executor執(zhí)行SQL語句的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
redis.clients.jedis.exceptions.JedisAskDataException異常解決
redis.clients.jedis.exceptions.JedisAskDataExceptio異常是在使用Jedis客戶端與Redis集群交互時(shí)遇到的一種重定向異常,本文就來介紹一下解決方法,感興趣的可以了解一下2024-05-05RocketMQ中消費(fèi)者的消費(fèi)進(jìn)度管理
這篇文章主要介紹了RocketMQ中消費(fèi)者的消費(fèi)進(jìn)度管理,業(yè)務(wù)實(shí)現(xiàn)消費(fèi)回調(diào)的時(shí)候,當(dāng)且僅當(dāng)此回調(diào)函數(shù)返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS ,RocketMQ才會(huì)認(rèn)為這批消息(默認(rèn)是1條)是消費(fèi)完成的,需要的朋友可以參考下2023-10-10Hibernate實(shí)現(xiàn)悲觀鎖和樂觀鎖代碼介紹
這篇文章主要介紹了Hibernate實(shí)現(xiàn)悲觀鎖和樂觀鎖的有關(guān)內(nèi)容,涉及hibernate的隔離機(jī)制,以及實(shí)現(xiàn)悲觀鎖和樂觀鎖的代碼實(shí)現(xiàn),需要的朋友可以了解下。2017-09-09Java concurrency之Condition條件_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Condition的作用是對(duì)鎖進(jìn)行更精確的控制。下面通過本文給大家分享Java concurrency之Condition條件的相關(guān)知識(shí),非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-06-06Java 如何從list中刪除符合條件的數(shù)據(jù)
這篇文章主要介紹了Java 如何從list中刪除符合條件的數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11SpringBoot 引?MybatisGenerator的實(shí)現(xiàn)步驟
本文主要介紹了SpringBoot 引?MybatisGenerator的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02解決Java 部署Tomcat時(shí)使用jni和jna調(diào)用DLL文件的問題
這篇文章主要介紹了解決Java 部署Tomcat時(shí)使用jni和jna調(diào)用DLL文件的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11Java類的序列化版本唯一標(biāo)識(shí)符serialVersionUID使用
serialVersionUID是一個(gè)類的序列化版本唯一標(biāo)識(shí)符,用于確保在反序列化過程中類的實(shí)例與序列化文件中的類版本相匹配,它在版本兼容性和安全性方面起著關(guān)鍵作用2025-01-01如何寫好一個(gè)Spring組件的實(shí)現(xiàn)步驟
這篇文章主要介紹了如何寫好一個(gè)Spring組件的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06