上一篇我們從整體上講述了MyBatis的整個工作流程,也知道了我們在執行Sql之前,需要先獲取SqlSession物件,但是我們也提到了SqlSession下面還有四大物件,所以SqlSession只是個甩手掌櫃,真正幹活的卻是Executor等四大物件:Executor,StatementHandler,ParameterHandler,ResultSetHandler。那麼本篇文章就讓我們來仔細分析一下這四大物件。
首先我們先來建立一個MyBatis的整體認知,下面就是MyBatis的一個整體分層架構圖:
我們今天要講解的四大天王物件就是核心處理層的四大物件,接下來就讓我們逐一進行分析
Executor就是真正用來執行Sql語句的物件,我們呼叫SqlSession中的方法,最終實際上都是通過Executor來完成的。我們先來看一下Executor的類圖關係:
這裡面其實用到了模板方法模式。頂層介面Executor定義了一系列規範,而在抽象類BaseExecutor中將一些固定不變的方法進行了封裝,並定義了一下抽象方法待子類實現。
BaseExecutor是一個抽象類,除了下面的四個方法是抽象方法,其餘所有方法都是一些如獲取快取,事務提交,獲取事務等公共操作,所以就直接被實現了。
如下圖所示,紅框之內的四個方法就是抽象方法:
我們在講述MyBatis核心設定的文章中提到,組態檔中的setting標籤內有一個屬性defaultExecutorType,有三種執行型別:SIMPLE,REUSE,BATCH。如果不設定則預設就是SIMPLE。這三種型別就是對應了BaseExecutor的三個子類:
SimpleExecutor,ReuseExecutor和BatchExecutor。
SimpleExecutor是最簡單的一個執行器,沒有任何特殊的,就是實現了BaseExecutor中的四個抽象方法。
我們來看其中一個doQuery()方法,可以看到沒有任何特殊邏輯,就是很常規的流程操作:
其中初始化Statement物件我們為了對比,也進去看一下:
我們再來看一個doFlushStatements()方法
這裡什麼都沒做,直接返回了一個空List
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從名字上也可以看出來,這是一個支援批次操作的執行器。
如果說大家都用過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);
}
這種方式會把大部分時間消耗在網路連線通訊上,一般不建議使用。
新建測試類:
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標籤的用法:
<select id="test">
select * from xxx where id in
<foreach collection="list" item="item" open="(" close=")" separator=",">
#{item.xxx}
</foreach>
</select>
我們把上面的普通例子中獲取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是ResultLoaderMap(懶載入時會使用)內的一個內部類,沒有任何具體實現,一般我們不會主動去使用。
這個執行器和快取有關,在這裡我們先不展開,下一篇講述快取實現原理的時候再來分析
StatementHandler是資料庫對談器,專門用來處理資料庫對談的。StatementHandler內運用了介面卡模式和策略模式的思想
類圖結構和Executor非常相似,如下圖所示:
這個介面中的方法也相對較少,prepare方法是用來初始化具體Statement物件的:
BaseStatementHandler是一個抽象類,實現了StatementHandler中的所有方法,只留下了一個初始化Statement物件方法留給子類實現。
SimpleStatementHandler對應JDBC的Statement,是一種非預編譯語句,所以引數中是沒有預留位置的,相當於引數中會用$符號
PreparedStatementHandler對應JDBC的PrepareStatement語句,是一種預編譯,引數會有預留位置,預編譯可以防止SQL隱碼攻擊
CallableStatementHandler依賴於JDBC的Callablement,用來呼叫儲存過程語句
RoutingStatementHandler這個從名字上可以看出來,只是起到了一個路由作用,會根據statement型別來生成相對應的Statement物件:
ParameterHandler是一個引數處理器,主要是用來對預編譯語句進行引數設定額,只有一個預設實現類DefaultParameterHandler。ParameterHandler中只定義了兩個方法,一個獲取引數,一個設定引數:
ResultHandler是一個結果處理器,StatementHandler完成了查詢之後,最終就是通過ResultHandler來實現結果集對映,ResultSetHandler介面中只定義了3個方法用來處理結果,而這三個方法對應了三種返回結果:
ResultHandler也預設提供了一個實現類:DefaultResultSetHandler。一般我們平常用的最多的就是通過handleResultSets來實現結果集轉換,這個方法的大致思路我們上一篇文章已經分析過了,在這裡就不重複展開。
經過這篇文章的分析,我想大家可以體會到SqlSession只是個甩手掌櫃的意思,因為SqlSession只是一個對外介面,實際真正幹活的卻是Executor等四大物件:Executor,StatementHandler,ParameterHandler,ResultSetHandler。本文的重點講述了Executor物件,並對比了三種常用批次操作的使用方法,相信通過這篇文章的學習大家對MyBatis的執行流程可以有更深一步的瞭解,掌握了這四大物件,後面就會更容易理解MyBatis的外掛實現原理。
請持續關注我後續文章,MyBatis後續文章系列計劃中至少還有三篇,分別會分析快取實現原理,外掛實現原理,和紀錄檔管理相關知識。
請關注我,和孤狼一起學習進步。