前言
上一篇我們從整體上講述了MyBatis的整個(gè)工作流程,也知道了我們在執(zhí)行Sql之前,需要先獲取SqlSession對象,但是我們也提到了SqlSession下面還有四大對象,所以SqlSession只是個(gè)甩手掌柜,真正干活的卻是Executor等四大對象:Executor,StatementHandler,ParameterHandler,ResultSetHandler。那么本篇文章就讓我們來仔細(xì)分析一下這四大對象。
MyBatis架構(gòu)分層
首先我們先來建立一個(gè)MyBatis的整體認(rèn)知,下面就是MyBatis的一個(gè)整體分層架構(gòu)圖:
- 接口層
接口層的核心對象就是SqlSession,SqlSession是應(yīng)用和MyBatis打交道的橋梁,SqlSession上定義了一系列數(shù)據(jù)庫操作方法,然后在收到請求的時(shí)候再去調(diào)用核心處理層模塊來完成具體操作。 - 核心處理層
真正和數(shù)據(jù)庫相關(guān)操作都是在核心層完成的,核心層主要做了以下4件事:
1、將接口中傳入的參數(shù)解析并且映射成為JDBC
2、解析xml文件中的SQL語句,包括參數(shù)的插入和動(dòng)態(tài)SQL的生成
3、執(zhí)行SQL語句
4、處理結(jié)果集,并且映射成Java對象
PS:插件也屬于核心層,因?yàn)椴寮褪菙r截核心處理層對象 - 基礎(chǔ)支持層
基礎(chǔ)支持層就是封裝一些底層操作用來處理核心層的功能
我們今天要講解的四大天王對象就是核心處理層的四大對象,接下來就讓我們逐一進(jìn)行分析
Executor
Executor就是真正用來執(zhí)行Sql語句的對象,我們調(diào)用SqlSession中的方法,最終實(shí)際上都是通過Executor來完成的。我們先來看一下Executor的類圖關(guān)系:
這里面其實(shí)用到了[模板方法模式]。頂層接口Executor定義了一系列規(guī)范,而在抽象類BaseExecutor中將一些固定不變的方法進(jìn)行了封裝,并定義了一下抽象方法待子類實(shí)現(xiàn)。
BaseExecutor
BaseExecutor是一個(gè)抽象類,除了下面的四個(gè)方法是抽象方法,其余所有方法都是一些如獲取緩存,事務(wù)提交,獲取事務(wù)等公共操作,所以就直接被實(shí)現(xiàn)了。
如下圖所示,紅框之內(nèi)的四個(gè)方法就是抽象方法:
- doFlushStatements():刷新Statement對象
- doQuery():執(zhí)行查詢語句并返回List
- doQueryCursor():執(zhí)行查詢語句并返回Cursor對象
- doUpdate():執(zhí)行更新操作
我們在講述MyBatis核心配置的文章中提到,配置文件中的setting標(biāo)簽內(nèi)有一個(gè)屬性defaultExecutorType,有三種執(zhí)行類型:SIMPLE,REUSE,BATCH。如果不配置則默認(rèn)就是SIMPLE。這三種類型就是對應(yīng)了BaseExecutor的三個(gè)子類:
SimpleExecutor,ReuseExecutor和BatchExecutor。
SimpleExecutor
SimpleExecutor是最簡單的一個(gè)執(zhí)行器,沒有任何特殊的,就是實(shí)現(xiàn)了BaseExecutor中的四個(gè)抽象方法。
我們來看其中一個(gè)doQuery()方法,可以看到?jīng)]有任何特殊邏輯,就是很常規(guī)的流程操作:
其中初始化Statement對象我們?yōu)榱藢Ρ?,也進(jìn)去看一下:
我們再來看一個(gè)doFlushStatements()方法
[圖片上傳失敗...(image-8eb5e5-1604376819050)]
這里什么都沒做,直接返回了一個(gè)空List
ReuseExecutor
ReuseExecutor相比較于SimpleExecutor做了一點(diǎn)優(yōu)化,那就是將Statement對象進(jìn)行了緩存處理,不會(huì)每次都創(chuàng)建Statement對象,這樣做的話減少了SQL預(yù)編譯和創(chuàng)建對象的開銷。
ReuseExecutor中的查詢和更新方法和SimpleExecutor完全一樣,而其中的差別就在于創(chuàng)建Statement對象上,我們進(jìn)去ReuseExecutor的prepareStatement方法:
我們可以看到區(qū)別就是多了一個(gè)從緩存中獲取Statement對象的邏輯,用來達(dá)到復(fù)用Statement對象的目的。
其中g(shù)etStatement是通過ReuseExecutor內(nèi)的一個(gè)HashMap屬性來獲取Statement對象,其中key值就是我們執(zhí)行的sql語句:
[圖片上傳失敗...(image-1ba2c8-1604376819050)]
我們再來看看doFlushStatements方法,可以看到,這里面會(huì)遍歷map將Statement關(guān)閉,并清空map,看到這里,大家應(yīng)該就明白了為什么SimpleExecutor內(nèi)這個(gè)方法直接返回的是空,因?yàn)镾impleExecutor方法沒有Statement需要關(guān)閉。
PS:doFlushStatements方法在BaseExecutor中的commit(),rollback(),close()方法中會(huì)被調(diào)用(即:事務(wù)提交,事務(wù)回滾,事務(wù)關(guān)閉三個(gè)方法)。
BatchExecutor
BatchExecutor從名字上也可以看出來,這是一個(gè)支持批量操作的執(zhí)行器。
如果說大家都用過jdbc就知道,jdbc是支持批量操作的,有一個(gè)executeBatch()方法用來執(zhí)行批量操作,但是有一個(gè)前提就是執(zhí)行批量操作的sql除了參數(shù)不同,其他都應(yīng)該是相同的(關(guān)于這一點(diǎn),下面我們會(huì)舉例來說明)。
需要注意的是,批量操作只支持insert,update,delete語句,select語句是不支持的,所以BatchExecutor內(nèi)的doQuery方法和其他執(zhí)行器并沒有很大不同,區(qū)別就是在查詢之前會(huì)先調(diào)用flushStatements(),我們不做過多討論,主要看一下doUpdate方法:
下面是一些成員屬性:
這個(gè)方法的邏輯就是判斷相同模式的sql會(huì)共用同一個(gè)Statement對象,然后緩存到list內(nèi),需要注意的是它只會(huì)和前一個(gè)進(jìn)行比對,也就是說假如你有相同模式的2條sql,但是你中間先執(zhí)行了一條其他sql,那么就會(huì)產(chǎn)生3個(gè)Statement對象,從而無法共用了。
PS:上面的doUpdate中返回了一個(gè)數(shù):BATCH_UPDATE_RETURN_VALUE,這個(gè)數(shù)其實(shí)沒有什么特別含義,只需要返回一個(gè)沒有意義的負(fù)數(shù)就可以,表示代碼不知道執(zhí)行成功多少條。比如說直接返回-1,或者干脆直接返回Integer.MIN_VALUE都是沒有問題的,全憑個(gè)人喜好了。
接下來我們再看看doFlushStatements()方法:
這個(gè)方法就是去遍歷上面存儲(chǔ)好的Statement,依次調(diào)用Statement中的executeBatch方法。
三種常用批量插入方式
講到這里,我們就干脆扯開一點(diǎn),聊一聊MyBatis編程中常用的三種批量操作方式。
直接代碼循環(huán)
這是最簡單的一種,但也是效率最低的一種,如下簡單示例:
UserAddressMapper userAddressMapper = session.getMapper(UserAddressMapper.class);
for (UserAddress userAddress : userAddressList){
userAddressMapper.insert(userAddress);
}
這種方式會(huì)把大部分時(shí)間消耗在網(wǎng)絡(luò)連接通信上,一般不建議使用。
利用MyBatis中批量標(biāo)簽foreach處理
新建測試類:
package com.lonelyWolf.mybatis.batch;
import com.lonelyWolf.mybatis.mapper.UserAddressMapper;
import com.lonelyWolf.mybatis.model.UserAddress;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class TestBatchInsert {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
//讀取mybatis-config配置文件
InputStream inputStream = Resources.getResourceAsStream(resource);
//創(chuàng)建SqlSessionFactory對象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//創(chuàng)建SqlSession對象
SqlSession session = sqlSessionFactory.openSession();
try {
List<UserAddress> userAddressList = new ArrayList<>();
UserAddress userAddr = new UserAddress();
userAddr.setAddress("廣東深圳");
userAddressList.add(userAddr);
UserAddress userAddr2 = new UserAddress();
userAddr2.setAddress("廣東廣州");
userAddressList.add(userAddr2);
UserAddressMapper userAddressMapper = session.getMapper(UserAddressMapper.class);
userAddressMapper.batchInsert(userAddressList);
session.commit();
}finally {
session.close();
}
}
}
Mapper接口新增如下方法:
int batchInsert(List<UserAddress> userAddresses);
XML文件如下:
<insert id="batchInsert">
insert into lw_user_address (address) values
<foreach collection="list" item="item" separator=",">
(#{item.address})
</foreach>
</insert>
執(zhí)行之后輸出如下語句:

順便我們介紹一下foreach標(biāo)簽的用法:
- collection
表示待循環(huán)的對象。當(dāng)參數(shù)為List時(shí),默認(rèn)"list",參數(shù)為數(shù)組時(shí),默認(rèn)"array"。但是當(dāng)我們在Mapper接口中使用@Param(“xxx”)時(shí),默認(rèn)的list,array將會(huì)失效,必須使用我們自己設(shè)置的參數(shù)名。 還有一種特殊情況就是假如集合里面有集合或者對象里面有集合,那么可以使用collection=“xxx.屬性名”。 - item
表示當(dāng)前循環(huán)中的元素。 - open/close,表示循環(huán)體開始和結(jié)束位置插入的符號,一般成對出現(xiàn),in語句使用較多,如:
<select id="test">
select * from xxx where id in
<foreach collection="list" item="item" open="(" close=")" separator=",">
#{item.xxx}
</foreach>
</select>
- separator:表示每個(gè)循環(huán)之后的分割符號,可參考上面的例子
- index:當(dāng)前元素在集合的下標(biāo),如果是map則是map的key值,這個(gè)參數(shù)一般用的相對較少。
BatchExecutor插入
我們把上面的普通例子中獲取Session的例子改寫一下:
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
然后執(zhí)行之后輸出sql如下:

可以看到,這兩條語句就是相同模式的sql,只是參數(shù)不同,所以直接執(zhí)行一次。
我們把上面的例子改寫一下:
UserAddress userAddr = new UserAddress();
userAddr.setAddress("廣東深圳");
userAddr.setId(1);
userAddressList.add(userAddr);
UserAddress userAddr2 = new UserAddress();
userAddr2.setAddress("廣東廣州");
userAddr2.setId(2);
userAddressList.add(userAddr2);
UserAddressMapper userAddressMapper = session.getMapper(UserAddressMapper.class);
userAddressMapper.insert(userAddr);//sql-1
userAddressMapper.insert10(userAddr2);//sql-10
userAddressMapper.insert(userAddr);//sql-1
insert和insert10分別對應(yīng)如下語句(一條是1個(gè)參數(shù),一條是2個(gè)參數(shù)):
<insert id="insert" parameterType="com.lonelyWolf.mybatis.model.UserAddress" useGeneratedKeys="true" keyProperty="address">
insert into lw_user_address (address) values (#{address})
</insert>
<insert id="insert10" parameterType="com.lonelyWolf.mybatis.model.UserAddress" useGeneratedKeys="true" keyProperty="address">
insert into lw_user_address (id,address) values (#{id},#{address})
</insert>
上面就是有兩種sql模型,理論上應(yīng)該執(zhí)行2次,但是我們根據(jù)源碼知道,因?yàn)閕nsert語句中間被insert10隔開了,所以實(shí)際上sql-1也是不能復(fù)用的,也就是會(huì)執(zhí)行3次:
PS:這三種批量執(zhí)行的效率有興趣的可以自己去測試一下,效率最高的應(yīng)該是foreach標(biāo)簽的形式,網(wǎng)上有其他
ClosedExecutor
ClosedExecutor是ResultLoaderMap(懶加載時(shí)會(huì)使用)內(nèi)的一個(gè)內(nèi)部類,沒有任何具體實(shí)現(xiàn),一般我們不會(huì)主動(dòng)去使用。
CachingExecutor
這個(gè)執(zhí)行器和緩存有關(guān),在這里我們先不展開,下一篇講述緩存實(shí)現(xiàn)原理的時(shí)候再來分析
StatementHandler
StatementHandler是數(shù)據(jù)庫會(huì)話器,專門用來處理數(shù)據(jù)庫會(huì)話的。StatementHandler內(nèi)運(yùn)用了適配器模式和策略模式的思想
類圖結(jié)構(gòu)和Executor非常相似,如下圖所示:
這個(gè)接口中的方法也相對較少,prepare方法是用來初始化具體Statement對象的:
BaseStatementHandler
BaseStatementHandler是一個(gè)抽象類,實(shí)現(xiàn)了StatementHandler中的所有方法,只留下了一個(gè)初始化Statement對象方法留給子類實(shí)現(xiàn)。
SimpleStatementHandler
SimpleStatementHandler對應(yīng)JDBC的Statement,是一種非預(yù)編譯語句,所以參數(shù)中是沒有占位符的,相當(dāng)于參數(shù)中會(huì)用$符號
PreparedStatementHandler
PreparedStatementHandler對應(yīng)JDBC的PrepareStatement語句,是一種預(yù)編譯,參數(shù)會(huì)有占位符,預(yù)編譯可以防止SQL注入
CallableStatementHandler
CallableStatementHandler依賴于JDBC的Callablement,用來調(diào)用存儲(chǔ)過程語句
RoutingStatementHandler
RoutingStatementHandler這個(gè)從名字上可以看出來,只是起到了一個(gè)路由作用,會(huì)根據(jù)statement類型來生成相對應(yīng)的Statement對象:
ParameterHandler
ParameterHandler是一個(gè)參數(shù)處理器,主要是用來對預(yù)編譯語句進(jìn)行參數(shù)設(shè)置額,只有一個(gè)默認(rèn)實(shí)現(xiàn)類DefaultParameterHandler。ParameterHandler中只定義了兩個(gè)方法,一個(gè)獲取參數(shù),一個(gè)設(shè)置參數(shù):
ResultSetHandler
ResultHandler是一個(gè)結(jié)果處理器,StatementHandler完成了查詢之后,最終就是通過ResultHandler來實(shí)現(xiàn)結(jié)果集映射,ResultSetHandler接口中只定義了3個(gè)方法用來處理結(jié)果,而這三個(gè)方法對應(yīng)了三種返回結(jié)果:
ResultHandler也默認(rèn)提供了一個(gè)實(shí)現(xiàn)類:DefaultResultSetHandler。一般我們平常用的最多的就是通過handleResultSets來實(shí)現(xiàn)結(jié)果集轉(zhuǎn)換,這個(gè)方法的大致思路我們上一篇文章已經(jīng)分析過了,在這里就不重復(fù)展開。
總結(jié)
經(jīng)過這篇文章的分析,我想大家可以體會(huì)到SqlSession只是個(gè)甩手掌柜的意思,因?yàn)镾qlSession只是一個(gè)對外接口,實(shí)際真正干活的卻是Executor等四大對象:Executor,StatementHandler,ParameterHandler,ResultSetHandler。本文的重點(diǎn)講述了Executor對象,并對比了三種常用批量操作的使用方法,相信通過這篇文章的學(xué)習(xí)大家對MyBatis的執(zhí)行流程可以有更深一步的了解,掌握了這四大對象,后面就會(huì)更容易理解MyBatis的插件實(shí)現(xiàn)原理。
請持續(xù)關(guān)注我后續(xù)文章,MyBatis后續(xù)文章系列計(jì)劃中至少還有三篇,分別會(huì)分析緩存實(shí)現(xiàn)原理,插件實(shí)現(xiàn)原理,和日志管理相關(guān)知識(shí)。