【MyBatis系列7】原來SqlSession只是個(gè)甩手掌柜,真正干活的卻是Executor等四大對象

前言

上一篇我們從整體上講述了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,ReuseExecutorBatchExecutor。

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í)行之后輸出如下語句:


image.png

順便我們介紹一下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如下:


image.png

可以看到,這兩條語句就是相同模式的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í)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容