【MyBatis系列7】原來SqlSession只是個甩手掌櫃,真正幹活的卻是Executor等四大物件

2020-09-28 09:02:44

前言

上一篇我們從整體上講述了MyBatis的整個工作流程,也知道了我們在執行Sql之前,需要先獲取SqlSession物件,但是我們也提到了SqlSession下面還有四大物件,所以SqlSession只是個甩手掌櫃,真正幹活的卻是Executor等四大物件:Executor,StatementHandler,ParameterHandler,ResultSetHandler。那麼本篇文章就讓我們來仔細分析一下這四大物件。

MyBatis架構分層

首先我們先來建立一個MyBatis的整體認知,下面就是MyBatis的一個整體分層架構圖:
在這裡插入圖片描述

  • 介面層
    介面層的核心物件就是SqlSession,SqlSession是應用和MyBatis打交道的橋樑,SqlSession上定義了一系列資料庫操作方法,然後在收到請求的時候再去呼叫核心處理層模組來完成具體操作。
  • 核心處理層
    真正和資料庫相關操作都是在核心層完成的,核心層主要做了以下4件事:
    1、將介面中傳入的引數解析並且對映成為JDBC
    2、解析xml檔案中的SQL語句,包括引數的插入和動態SQL的生成
    3、執行SQL語句
    4、處理結果集,並且對映成Java物件
    PS:外掛也屬於核心層,因為外掛就是攔截核心處理層物件
  • 基礎支援層
    基礎支援層就是封裝一些底層操作用來處理核心層的功能

我們今天要講解的四大天王物件就是核心處理層的四大物件,接下來就讓我們逐一進行分析

Executor

Executor就是真正用來執行Sql語句的物件,我們呼叫SqlSession中的方法,最終實際上都是通過Executor來完成的。我們先來看一下Executor的類圖關係:
在這裡插入圖片描述
這裡面其實用到了模板方法模式。頂層介面Executor定義了一系列規範,而在抽象類BaseExecutor中將一些固定不變的方法進行了封裝,並定義了一下抽象方法待子類實現。

BaseExecutor

BaseExecutor是一個抽象類,除了下面的四個方法是抽象方法,其餘所有方法都是一些如獲取快取,事務提交,獲取事務等公共操作,所以就直接被實現了。
如下圖所示,紅框之內的四個方法就是抽象方法:
在這裡插入圖片描述

  • doFlushStatements():重新整理Statement物件
  • doQuery():執行查詢語句並返回List
  • doQueryCursor():執行查詢語句並返回Cursor物件
  • doUpdate():執行更新操作

我們在講述MyBatis核心設定的文章中提到,組態檔中的setting標籤內有一個屬性defaultExecutorType,有三種執行型別:SIMPLEREUSEBATCH。如果不設定則預設就是SIMPLE。這三種型別就是對應了BaseExecutor的三個子類:
SimpleExecutorReuseExecutorBatchExecutor

SimpleExecutor

SimpleExecutor是最簡單的一個執行器,沒有任何特殊的,就是實現了BaseExecutor中的四個抽象方法。
我們來看其中一個doQuery()方法,可以看到沒有任何特殊邏輯,就是很常規的流程操作:
在這裡插入圖片描述
其中初始化Statement物件我們為了對比,也進去看一下:
在這裡插入圖片描述
我們再來看一個doFlushStatements()方法
在這裡插入圖片描述
這裡什麼都沒做,直接返回了一個空List

ReuseExecutor

ReuseExecutor相比較於SimpleExecutor做了一點優化,那就是將Statement物件進行了快取處理,不會每次都建立Statement物件,這樣做的話減少了SQL預編譯和建立物件的開銷。

ReuseExecutor中的查詢和更新方法和SimpleExecutor完全一樣,而其中的差別就在於建立Statement物件上,我們進去ReuseExecutor的prepareStatement方法:
在這裡插入圖片描述
我們可以看到區別就是多了一個從快取中獲取Statement物件的邏輯,用來達到複用Statement物件的目的。
其中getStatement是通過ReuseExecutor內的一個HashMap屬性來獲取Statement物件,其中key值就是我們執行的sql語句:
在這裡插入圖片描述
我們再來看看doFlushStatements方法,可以看到,這裡面會遍歷map將Statement關閉,並清空map,看到這裡,大家應該就明白了為什麼SimpleExecutor內這個方法直接返回的是空,因為SimpleExecutor方法沒有Statement需要關閉。
在這裡插入圖片描述
PS:doFlushStatements方法在BaseExecutor中的commit(),rollback(),close()方法中會被呼叫(即:事務提交,事務回滾,事務關閉三個方法)。

BatchExecutor

BatchExecutor從名字上也可以看出來,這是一個支援批次操作的執行器。

如果說大家都用過jdbc就知道,jdbc是支援批次操作的,有一個executeBatch()方法用來執行批次操作,但是有一個前提就是執行批次操作的sql除了引數不同,其他都應該是相同的(關於這一點,下面我們會舉例來說明)。
需要注意的是,批次操作只支援insert,update,delete語句,select語句是不支援的,所以BatchExecutor內的doQuery方法和其他執行器並沒有很大不同,區別就是在查詢之前會先呼叫flushStatements(),我們不做過多討論,主要看一下doUpdate方法:
在這裡插入圖片描述
下面是一些成員屬性:
在這裡插入圖片描述
這個方法的邏輯就是判斷相同模式的sql會共用同一個Statement物件,然後快取到list內,需要注意的是它只會和前一個進行比對,也就是說假如你有相同模式的2條sql,但是你中間先執行了一條其他sql,那麼就會產生3個Statement物件,從而無法共用了。

PS:上面的doUpdate中返回了一個數:BATCH_UPDATE_RETURN_VALUE,這個數其實沒有什麼特別含義,只需要返回一個沒有意義的負數就可以,表示程式碼不知道執行成功多少條。比如說直接返回-1,或者乾脆直接返回Integer.MIN_VALUE都是沒有問題的,全憑個人喜好了。

接下來我們再看看doFlushStatements()方法:
在這裡插入圖片描述
這個方法就是去遍歷上面儲存好的Statement,依次呼叫Statement中的executeBatch方法。

三種常用批次插入方式

講到這裡,我們就乾脆扯開一點,聊一聊MyBatis程式設計中常用的三種批次操作方式。

直接程式碼迴圈

這是最簡單的一種,但也是效率最低的一種,如下簡單範例:

UserAddressMapper userAddressMapper = session.getMapper(UserAddressMapper.class);
for (UserAddress userAddress : userAddressList){
     userAddressMapper.insert(userAddress);
 }

這種方式會把大部分時間消耗在網路連線通訊上,一般不建議使用。

利用MyBatis中批次標籤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);
        //建立SqlSessionFactory物件
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //建立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>

執行之後輸出如下語句:
在這裡插入圖片描述
順便我們介紹一下foreach標籤的用法:

  • collection
    表示待迴圈的物件。當引數為List時,預設"list",引數為陣列時,預設"array"。但是當我們在Mapper介面中使用@Param(「xxx」)時,預設的list,array將會失效,必須使用我們自己設定的引數名。 還有一種特殊情況就是假如集合裡面有集合或者物件裡面有集合,那麼可以使用collection=「xxx.屬性名」。
  • item
    表示當前迴圈中的元素。
  • open/close,表示迴圈體開始和結束位置插入的符號,一般成對出現,in語句使用較多,如:
<select id="test">
       select * from xxx where id in 
     <foreach collection="list" item="item" open="(" close=")" separator=",">
         #{item.xxx}
     </foreach>
   </select>
  • separator:表示每個迴圈之後的分割符號,可參考上面的例子
  • index:當前元素在集合的下標,如果是map則是map的key值,這個引數一般用的相對較少。
BatchExecutor插入

我們把上面的普通例子中獲取Session的例子改寫一下:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);

如果想詳細瞭解openSession方法引數的,可以點選這裡。然後執行之後輸出sql如下:
在這裡插入圖片描述
可以看到,這兩條語句就是相同模式的sql,只是引數不同,所以直接執行一次。

我們把上面的例子改寫一下:

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分別對應如下語句(一條是1個引數,一條是2個引數):

<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模型,理論上應該執行2次,但是我們根據原始碼知道,因為insert語句中間被insert10隔開了,所以實際上sql-1也是不能複用的,也就是會執行3次:
在這裡插入圖片描述

PS:這三種批次執行的效率有興趣的可以自己去測試一下,效率最高的應該是foreach標籤的形式,網上有其他

ClosedExecutor

ClosedExecutor是ResultLoaderMap(懶載入時會使用)內的一個內部類,沒有任何具體實現,一般我們不會主動去使用。

CachingExecutor

這個執行器和快取有關,在這裡我們先不展開,下一篇講述快取實現原理的時候再來分析

StatementHandler

StatementHandler是資料庫對談器,專門用來處理資料庫對談的。StatementHandler內運用了介面卡模式策略模式的思想
類圖結構和Executor非常相似,如下圖所示:

在這裡插入圖片描述
這個介面中的方法也相對較少,prepare方法是用來初始化具體Statement物件的:
在這裡插入圖片描述

BaseStatementHandler

BaseStatementHandler是一個抽象類,實現了StatementHandler中的所有方法,只留下了一個初始化Statement物件方法留給子類實現。

SimpleStatementHandler

SimpleStatementHandler對應JDBC的Statement,是一種非預編譯語句,所以引數中是沒有預留位置的,相當於引數中會用$符號

PreparedStatementHandler

PreparedStatementHandler對應JDBC的PrepareStatement語句,是一種預編譯,引數會有預留位置,預編譯可以防止SQL隱碼攻擊

CallableStatementHandler

CallableStatementHandler依賴於JDBC的Callablement,用來呼叫儲存過程語句

RoutingStatementHandler

RoutingStatementHandler這個從名字上可以看出來,只是起到了一個路由作用,會根據statement型別來生成相對應的Statement物件:
在這裡插入圖片描述

ParameterHandler

ParameterHandler是一個引數處理器,主要是用來對預編譯語句進行引數設定額,只有一個預設實現類DefaultParameterHandler。ParameterHandler中只定義了兩個方法,一個獲取引數,一個設定引數:
在這裡插入圖片描述

ResultSetHandler

ResultHandler是一個結果處理器,StatementHandler完成了查詢之後,最終就是通過ResultHandler來實現結果集對映,ResultSetHandler介面中只定義了3個方法用來處理結果,而這三個方法對應了三種返回結果:
在這裡插入圖片描述ResultHandler也預設提供了一個實現類:DefaultResultSetHandler。一般我們平常用的最多的就是通過handleResultSets來實現結果集轉換,這個方法的大致思路我們上一篇文章已經分析過了,在這裡就不重複展開。

總結

經過這篇文章的分析,我想大家可以體會到SqlSession只是個甩手掌櫃的意思,因為SqlSession只是一個對外介面,實際真正幹活的卻是Executor等四大物件:Executor,StatementHandler,ParameterHandler,ResultSetHandler。本文的重點講述了Executor物件,並對比了三種常用批次操作的使用方法,相信通過這篇文章的學習大家對MyBatis的執行流程可以有更深一步的瞭解,掌握了這四大物件,後面就會更容易理解MyBatis的外掛實現原理。

請持續關注我後續文章,MyBatis後續文章系列計劃中至少還有三篇,分別會分析快取實現原理,外掛實現原理,和紀錄檔管理相關知識。

請關注我,和孤狼一起學習進步