面試官問你MyBatis SQL是如何執行的?把這篇文章甩給他

cxuann發表於2020-02-07

初識 MyBatis

MyBatis 是第一個支援自定義 SQL、儲存過程和高階對映的類持久框架。MyBatis 消除了大部分 JDBC 的樣板程式碼、手動設定引數以及檢索結果。MyBatis 能夠支援簡單的 XML 和註解配置規則。使 Map 介面和 POJO 類對映到資料庫欄位和記錄。

MyBatis 的特點

那麼 MyBatis 具有什麼特點呢?或許我們可以從如下幾個方面來描述

  • MyBatis 中的 SQL 語句和主要業務程式碼分離,我們一般會把 MyBatis 中的 SQL 語句統一放在 XML 配置檔案中,便於統一維護。

  • 解除 SQL 與程式程式碼的耦合,通過提供 DAO 層,將業務邏輯和資料訪問邏輯分離,使系統的設計更清晰,更易維護,更易單元測試。SQL 和程式碼的分離,提高了可維護性。

  • MyBatis 比較簡單和輕量

本身就很小且簡單。沒有任何第三方依賴,只要通過配置 jar 包,或者如果你使用 Maven 專案的話只需要配置 Maven 以來就可以。易於使用,通過文件和原始碼,可以比較完全的掌握它的設計思路和實現。

  • 遮蔽樣板程式碼

MyBatis 回遮蔽原始的 JDBC 樣板程式碼,讓你把更多的精力專注於 SQL 的書寫和屬性-欄位對映上。

  • 編寫原生 SQL,支援多表關聯

MyBatis 最主要的特點就是你可以手動編寫 SQL 語句,能夠支援多表關聯查詢。

  • 提供對映標籤,支援物件與資料庫的 ORM 欄位關係對映

ORM 是什麼?物件關係對映(Object Relational Mapping,簡稱ORM) ,是通過使用描述物件和資料庫之間對映的後設資料,將面嚮物件語言程式中的物件自動持久化到關聯式資料庫中。本質上就是將資料從一種形式轉換到另外一種形式。

  • 提供 XML 標籤,支援編寫動態 SQL。

你可以使用 MyBatis XML 標籤,起到 SQL 模版的效果,減少繁雜的 SQL 語句,便於維護。

MyBatis 整體架構

MyBatis 最上面是介面層,介面層就是開發人員在 Mapper 或者是 Dao 介面中的介面定義,是查詢、新增、更新還是刪除操作;中間層是資料處理層,主要是配置 Mapper -> XML 層級之間的引數對映,SQL 解析,SQL 執行,結果對映的過程。上述兩種流程都由基礎支援層來提供功能支撐,基礎支援層包括連線管理,事務管理,配置載入,快取處理等。

介面層

在不與Spring 整合的情況下,使用 MyBatis 執行資料庫的操作主要如下:

InputStream is = Resources.getResourceAsStream("myBatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
sqlSession = factory.openSession();

其中的SqlSessionFactory,SqlSession是 MyBatis 介面的核心類,尤其是 SqlSession,這個介面是MyBatis 中最重要的介面,這個介面能夠讓你執行命令,獲取對映,管理事務。

資料處理層

  • 配置解析

在 Mybatis 初始化過程中,會載入 mybatis-config.xml 配置檔案、對映配置檔案以及 Mapper 介面中的註解資訊,解析後的配置資訊會形成相應的物件並儲存到 Configration 物件中。之後,根據該物件建立SqlSessionFactory 物件。待 Mybatis 初始化完成後,可以通過 SqlSessionFactory 建立 SqlSession 物件並開始資料庫操作。

  • SQL 解析與 scripting 模組

Mybatis 實現的動態 SQL 語句,幾乎可以編寫出所有滿足需要的 SQL。

Mybatis 中 scripting 模組會根據使用者傳入的引數,解析對映檔案中定義的動態 SQL 節點,形成資料庫能執行的SQL 語句。

  • SQL 執行

SQL 語句的執行涉及多個元件,包括 MyBatis 的四大核心,它們是: ExecutorStatementHandlerParameterHandlerResultSetHandler。SQL 的執行過程可以用下面這幅圖來表示

MyBatis 層級結構各個元件的介紹(這裡只是簡單介紹,具體介紹在後面):

  • SqlSession: ,它是 MyBatis 核心 API,主要用來執行命令,獲取對映,管理事務。接收開發人員提供 Statement Id 和引數。並返回操作結果。
  • Executor :執行器,是 MyBatis 排程的核心,負責 SQL 語句的生成以及查詢快取的維護。
  • StatementHandler : 封裝了JDBC Statement 操作,負責對 JDBC Statement 的操作,如設定引數、將Statement 結果集轉換成 List 集合。
  • ParameterHandler : 負責對使用者傳遞的引數轉換成 JDBC Statement 所需要的引數。
  • ResultSetHandler : 負責將 JDBC 返回的 ResultSet 結果集物件轉換成 List 型別的集合。
  • TypeHandler : 用於 Java 型別和 JDBC 型別之間的轉換。
  • MappedStatement : 動態 SQL 的封裝
  • SqlSource : 表示從 XML 檔案或註釋讀取的對映語句的內容,它建立將從使用者接收的輸入引數傳遞給資料庫的 SQL。
  • Configuration: MyBatis 所有的配置資訊都維持在 Configuration 物件之中。

基礎支援層

  • 反射模組

Mybatis 中的反射模組,對 Java 反射進行了很好的封裝,提供了簡易的 API,方便上層呼叫,並且對反射操作進行了一系列的優化,比如,快取了類的 後設資料(MetaClass)和物件的後設資料(MetaObject),提高了反射操作的效能。

  • 型別轉換模組

Mybatis 的別名機制,能夠簡化配置檔案,該機制是型別轉換模組的主要功能之一。型別轉換模組的另一個功能是實現 JDBC 型別與 Java 型別的轉換。在 SQL 語句繫結引數時,會將資料由 Java 型別轉換成 JDBC 型別;在對映結果集時,會將資料由 JDBC 型別轉換成 Java 型別。

  • 日誌模組

在 Java 中,有很多優秀的日誌框架,如 Log4j、Log4j2、slf4j 等。Mybatis 除了提供了詳細的日誌輸出資訊,還能夠整合多種日誌框架,其日誌模組的主要功能就是整合第三方日誌框架。

  • 資源載入模組

該模組主要封裝了類載入器,確定了類載入器的使用順序,並提供了載入類檔案和其它資原始檔的功能。

  • 解析器模組

該模組有兩個主要功能:一個是封裝了 XPath,為 Mybatis 初始化時解析 mybatis-config.xml配置檔案以及對映配置檔案提供支援;另一個為處理動態 SQL 語句中的佔位符提供支援。

  • 資料來源模組

Mybatis 自身提供了相應的資料來源實現,也提供了與第三方資料來源整合的介面。資料來源是開發中的常用元件之一,很多開源的資料來源都提供了豐富的功能,如連線池、檢測連線狀態等,選擇效能優秀的資料來源元件,對於提供ORM 框架以及整個應用的效能都是非常重要的。

  • 事務管理模組

一般地,Mybatis 與 Spring 框架整合,由 Spring 框架管理事務。但 Mybatis 自身對資料庫事務進行了抽象,提供了相應的事務介面和簡單實現。

  • 快取模組

Mybatis 中有一級快取二級快取,這兩級快取都依賴於快取模組中的實現。但是需要注意,這兩級快取與Mybatis 以及整個應用是執行在同一個 JVM 中的,共享同一塊記憶體,如果這兩級快取中的資料量較大,則可能影響系統中其它功能,所以需要快取大量資料時,優先考慮使用 Redis、Memcache 等快取產品。

  • Binding 模組

在呼叫 SqlSession 相應方法執行資料庫操作時,需要制定對映檔案中定義的 SQL 節點,如果 SQL 中出現了拼寫錯誤,那就只能在執行時才能發現。為了能儘早發現這種錯誤,Mybatis 通過 Binding 模組將使用者自定義的Mapper 介面與對映檔案關聯起來,系統可以通過呼叫自定義 Mapper 介面中的方法執行相應的 SQL 語句完成資料庫操作,從而避免上述問題。注意,在開發中,我們只是建立了 Mapper 介面,而並沒有編寫實現類,這是因為 Mybatis 自動為 Mapper 介面建立了動態代理物件。

MyBatis 核心元件

在認識了 MyBatis 並瞭解其基礎架構之後,下面我們來看一下 MyBatis 的核心元件,就是這些元件實現了從 SQL 語句到對映到 JDBC 再到資料庫欄位之間的轉換,執行 SQL 語句並輸出結果集。首先來認識 MyBatis 的第一個核心元件

SqlSessionFactory

對於任何框架而言,在使用該框架之前都要經歷過一系列的初始化流程,MyBatis 也不例外。MyBatis 的初始化流程如下

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSessionFactory.openSession();

上述流程中比較重要的一個物件就是SqlSessionFactory,SqlSessionFactory 是 MyBatis 框架中的一個介面,它主要負責的是

  • MyBatis 框架初始化操作
  • 為開發人員提供SqlSession 物件

SqlSessionFactory 有兩個實現類,一個是 SqlSessionManager 類,一個是 DefaultSqlSessionFactory 類

  • DefaultSqlSessionFactory : SqlSessionFactory 的預設實現類,是真正生產會話的工廠類,這個類的例項的生命週期是全域性的,它只會在首次呼叫時生成一個例項(單例模式),就一直存在直到伺服器關閉。

  • SqlSessionManager : 已被廢棄,原因大概是: SqlSessionManager 中需要維護一個自己的執行緒池,而使用MyBatis 更多的是要與 Spring 進行整合,並不會單獨使用,所以維護自己的 ThreadLocal 並沒有什麼意義,所以 SqlSessionManager 已經不再使用。

####SqlSessionFactory 的執行流程

下面來對 SqlSessionFactory 的執行流程來做一個分析

首先第一步是 SqlSessionFactory 的建立

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

從這行程式碼入手,首先建立了一個 SqlSessionFactoryBuilder 工廠,這是一個建造者模式的設計思想,由 builder 建造者來建立 SqlSessionFactory 工廠

然後呼叫 SqlSessionFactoryBuilder 中的 build 方法傳遞一個InputStream 輸入流,Inputstream 輸入流中就是你傳過來的配置檔案 mybatis-config.xml,SqlSessionFactoryBuilder 根據傳入的 InputStream 輸入流和environmentproperties屬性建立一個XMLConfigBuilder物件。SqlSessionFactoryBuilder 物件呼叫XMLConfigBuilder 的parse()方法,流程如下。

XMLConfigBuilder 會解析/configuration標籤,configuration 是 MyBatis 中最重要的一個標籤,下面流程會介紹 Configuration 標籤。

MyBatis 預設使用 XPath 來解析標籤,關於 XPath 的使用,參見 https://www.w3school.com.cn/xpath/index.asp

parseConfiguration 方法中,會對各個在 /configuration 中的標籤進行解析

重要配置

說一下這些標籤都是什麼意思吧

  • properties,外部屬性,這些屬性都是可外部配置且可動態替換的,既可以在典型的 Java 屬性檔案中配置,亦可通過 properties 元素的子元素來傳遞。
<properties>
    <property name="driver" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/test" />
    <property name="username" value="root" />
    <property name="password" value="root" />
</properties>

一般用來給 environment 標籤中的 dataSource 賦值

<environment id="development">
  <transactionManager type="JDBC" />
  <dataSource type="POOLED">
    <property name="driver" value="${driver}" />
    <property name="url" value="${url}" />
    <property name="username" value="${username}" />
    <property name="password" value="${password}" />
  </dataSource>
</environment>

還可以通過外部屬性進行配置,但是我們這篇文章以原理為主,不會介紹太多應用層面的操作。

  • settings ,MyBatis 中極其重要的配置,它們會改變 MyBatis 的執行時行為。

settings 中配置有很多,具體可以參考 https://mybatis.org/mybatis-3/zh/configuration.html#settings 詳細瞭解。這裡介紹幾個平常使用過程中比較重要的配置

屬性 描述
cacheEnabled 全域性地開啟或關閉配置檔案中的所有對映器已經配置的任何快取。
useGeneratedKeys 允許 JDBC 支援自動生成主鍵,需要驅動支援。 如果設定為 true 則這個設定強制使用自動生成主鍵。
lazyLoadingEnabled 延遲載入的全域性開關。當開啟時,所有關聯物件都會延遲載入。
jdbcTypeForNull 當沒有為引數提供特定的 JDBC 型別時,為空值指定 JDBC 型別。 某些驅動需要指定列的 JDBC 型別,多數情況直接用一般型別即可,比如 NULL、VARCHAR 或 OTHER。
defaultExecutorType 配置預設的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(prepared statements); BATCH 執行器將重用語句並執行批量更新。
localCacheScope MyBatis 利用本地快取機制(Local Cache)防止迴圈引用(circular references)和加速重複巢狀查詢。 預設值為 SESSION,這種情況下會快取一個會話中執行的所有查詢。 若設定值為 STATEMENT,本地會話僅用在語句執行上,對相同 SqlSession 的不同呼叫將不會共享資料
proxyFactory 指定 Mybatis 建立具有延遲載入能力的物件所用到的代理工具。
mapUnderscoreToCamelCase 是否開啟自動駝峰命名規則(camel case)對映,即從經典資料庫列名 A_COLUMN 到經典 Java 屬性名 aColumn 的類似對映。

一般使用如下配置

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
</settings>
  • typeAliases,型別別名,型別別名是為 Java 型別設定的一個名字。 它只和 XML 配置有關。
<typeAliases>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
</typeAliases>

當這樣配置時,Blog 可以用在任何使用 domain.blog.Blog 的地方。

  • typeHandlers,型別處理器,無論是 MyBatis 在預處理語句(PreparedStatement)中設定一個引數時,還是從結果集中取出一個值時, 都會用型別處理器將獲取的值以合適的方式轉換成 Java 型別。

org.apache.ibatis.type 包下有很多已經實現好的 TypeHandler,可以參考如下

你可以重寫型別處理器或建立你自己的型別處理器來處理不支援的或非標準的型別。

具體做法為:實現 org.apache.ibatis.type.TypeHandler 介面, 或繼承一個很方便的類 org.apache.ibatis.type.BaseTypeHandler, 然後可以選擇性地將它對映到一個 JDBC 型別。

  • objectFactory,物件工廠,MyBatis 每次建立結果物件的新例項時,它都會使用一個物件工廠(ObjectFactory)例項來完成。預設的物件工廠需要做的僅僅是例項化目標類,要麼通過預設構造方法,要麼在引數對映存在的時候通過引數構造方法來例項化。如果想覆蓋物件工廠的預設行為,則可以通過建立自己的物件工廠來實現。
public class ExampleObjectFactory extends DefaultObjectFactory {
  public Object create(Class type) {
    return super.create(type);
  }
  public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
    return super.create(type, constructorArgTypes, constructorArgs);
  }
  public void setProperties(Properties properties) {
    super.setProperties(properties);
  }
  public <T> boolean isCollection(Class<T> type) {
    return Collection.class.isAssignableFrom(type);
  }
}

然後需要在 XML 中配置此物件工廠

<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>
  • plugins,外掛開發,外掛開發是 MyBatis 設計人員給開發人員留給自行開發的介面,MyBatis 允許你在已對映語句執行過程中的某一點進行攔截呼叫。MyBatis 允許使用外掛來攔截的方法呼叫包括:Executor、ParameterHandler、ResultSetHandler、StatementHandler 介面,這幾個介面也是 MyBatis 中非常重要的介面,我們下面會詳細介紹這幾個介面。

  • environments,MyBatis 環境配置,MyBatis 可以配置成適應多種環境,這種機制有助於將 SQL 對映應用於多種資料庫之中。例如,開發、測試和生產環境需要有不同的配置;或者想在具有相同 Schema 的多個生產資料庫中 使用相同的 SQL 對映。

    這裡注意一點,雖然 environments 可以指定多個環境,但是 SqlSessionFactory 只能有一個,為了指定建立哪種環境,只要將它作為可選的引數傳遞給 SqlSessionFactoryBuilder 即可。

    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);
    

    環境配置如下

    <environments default="development">
      <environment id="development">
        <transactionManager type="JDBC">
          <property name="..." value="..."/>
        </transactionManager>
        <dataSource type="POOLED">
          <property name="driver" value="${driver}"/>
          <property name="url" value="${url}"/>
          <property name="username" value="${username}"/>
          <property name="password" value="${password}"/>
        </dataSource>
      </environment>
    </environments>
    
  • databaseIdProvider ,資料庫廠商標示,MyBatis 可以根據不同的資料庫廠商執行不同的語句,這種多廠商的支援是基於對映語句中的 databaseId 屬性。

    <databaseIdProvider type="DB_VENDOR">
      <property name="SQL Server" value="sqlserver"/>
      <property name="DB2" value="db2"/>
      <property name="Oracle" value="oracle" />
    </databaseIdProvider>
    
  • mappers,對映器,這是告訴 MyBatis 去哪裡找到這些 SQL 語句,mappers 對映配置有四種方式

    <!-- 使用相對於類路徑的資源引用 -->
    <mappers>
      <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
      <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
      <mapper resource="org/mybatis/builder/PostMapper.xml"/>
    </mappers>
    
    <!-- 使用完全限定資源定位符(URL) -->
    <mappers>
      <mapper url="file:///var/mappers/AuthorMapper.xml"/>
      <mapper url="file:///var/mappers/BlogMapper.xml"/>
      <mapper url="file:///var/mappers/PostMapper.xml"/>
    </mappers>
    
    <!-- 使用對映器介面實現類的完全限定類名 -->
    <mappers>
      <mapper class="org.mybatis.builder.AuthorMapper"/>
      <mapper class="org.mybatis.builder.BlogMapper"/>
      <mapper class="org.mybatis.builder.PostMapper"/>
    </mappers>
    
    <!-- 將包內的對映器介面實現全部註冊為對映器 -->
    <mappers>
      <package name="org.mybatis.builder"/>
    </mappers>
    

上面的一個個屬性都對應著一個解析方法,都是使用 XPath 把標籤進行解析,解析完成後返回一個 DefaultSqlSessionFactory 物件,它是 SqlSessionFactory 的預設實現類。這就是 SqlSessionFactoryBuilder 的初始化流程,通過流程我們可以看到,初始化流程就是對一個個 /configuration 標籤下子標籤的解析過程。

SqlSession

在 MyBatis 初始化流程結束,也就是 SqlSessionFactoryBuilder -> SqlSessionFactory 的獲取流程後,我們就可以通過 SqlSessionFactory 物件得到 SqlSession 然後執行 SQL 語句了。具體來看一下這個過程

在 SqlSessionFactory.openSession 過程中我們可以看到,會呼叫到 DefaultSqlSessionFactory 中的 openSessionFromDataSource 方法,這個方法主要建立了兩個與我們分析執行流程重要的物件,一個是 Executor 執行器物件,一個是 SqlSession 物件。執行器我們下面會說,現在來說一下 SqlSession 物件

SqlSession 物件是 MyBatis 中最重要的一個物件,這個介面能夠讓你執行命令,獲取對映,管理事務。SqlSession 中定義了一系列模版方法,讓你能夠執行簡單的 CRUD 操作,也可以通過 getMapper 獲取 Mapper 層,執行自定義 SQL 語句,因為 SqlSession 在執行 SQL 語句之前是需要先開啟一個會話,涉及到事務操作,所以還會有 commitrollbackclose 等方法。這也是模版設計模式的一種應用。

MapperProxy

MapperProxy 是 Mapper 對映 SQL 語句的關鍵物件,我們寫的 Dao 層或者 Mapper 層都是通過 MapperProxy 來和對應的 SQL 語句進行繫結的。下面我們就來解釋一下繫結過程

這就是 MyBatis 的核心繫結流程,我們可以看到 SqlSession 首先呼叫 getMapper 方法,我們剛才說到 SqlSession 是大哥級別的人物,只定義標準(有一句話是怎麼說的來著,一流的企業做標準,二流的企業做品牌,三流的企業做產品)。

SqlSession 不願意做的事情交給 Configuration 這個手下去做,但是 Configuration 也是有小弟的,它不願意做的事情直接甩給小弟去做,這個小弟是誰呢?它就是 MapperRegistry,馬上就到核心部分了。MapperRegistry 相當於專案經理,專案經理只從大面上把握專案進度,不需要知道手下的小弟是如何工作的,把任務完成了就好。最終真正幹活的還是 MapperProxyFactory。看到這段程式碼 Proxy.newProxyInstance ,你是不是有一種恍然大悟的感覺,如果你沒有的話,建議查閱一下動態代理的文章,這裡推薦一篇 (https://www.jianshu.com/p/95970b089360)

也就是說,MyBatis 中 Mapper 和 SQL 語句的繫結正是通過動態代理來完成的。

通過動態代理,我們就可以方便的在 Dao 層或者 Mapper 層定義介面,實現自定義的增刪改查操作了。那麼具體的執行過程是怎麼樣呢?上面只是繫結過程,彆著急,下面就來探討一下 SQL 語句的執行過程。

有一部分程式碼被遮擋,程式碼有些多,不過不影響我們看主要流程

MapperProxyFactory 會生成代理物件,這個物件就是 MapperProxy,最終會呼叫到 mapperMethod.execute 方法,execute 方法比較長,其實邏輯比較簡單,就是判斷是 插入更新刪除 還是 查詢 語句,其中如果是查詢的話,還會判斷返回值的型別,我們可以點進去看一下都是怎麼設計的。

很多程式碼其實可以忽略,只看我標出來的重點就好了,我們可以看到,不管你前面經過多少道關卡處理,最終都逃不過 SqlSession 這個老大制定的標準。

我們以 selectList 為例,來看一下下面的執行過程。

這是 DefaultSqlSession 中 selectList 的程式碼,我們可以看到出現了 executor,這是什麼呢?我們下面來解釋。

Executor

還記得我們之前的流程中提到了 Executor(執行器) 這個概念嗎?我們來回顧一下它第一次出現的位置。

由 Configuration 物件建立了一個 Executor 物件,這個 Executor 是幹嘛的呢?下面我們就來認識一下

Executor 的繼承結構

每一個 SqlSession 都會擁有一個 Executor 物件,這個物件負責增刪改查的具體操作,我們可以簡單的將它理解為 JDBC 中 Statement 的封裝版。 也可以理解為 SQL 的執行引擎,要幹活總得有一個發起人吧,可以把 Executor 理解為發起人的角色。

首先先從 Executor 的繼承體系來認識一下

如上圖所示,位於繼承體系最頂層的是 Executor 執行器,它有兩個實現類,分別是BaseExecutorCachingExecutor

BaseExecutor 是一個抽象類,這種通過抽象的實現介面的方式是介面卡設計模式之介面適配 的體現,是Executor 的預設實現,實現了大部分 Executor 介面定義的功能,降低了介面實現的難度。BaseExecutor 的子類有三個,分別是 SimpleExecutor、ReuseExecutor 和 BatchExecutor。

SimpleExecutor : 簡單執行器,是 MyBatis 中預設使用的執行器,每執行一次 update 或 select,就開啟一個Statement 物件,用完就直接關閉 Statement 物件(可以是 Statement 或者是 PreparedStatment 物件)

ReuseExecutor : 可重用執行器,這裡的重用指的是重複使用 Statement,它會在內部使用一個 Map 把建立的Statement 都快取起來,每次執行 SQL 命令的時候,都會去判斷是否存在基於該 SQL 的 Statement 物件,如果存在 Statement 物件並且對應的 connection 還沒有關閉的情況下就繼續使用之前的 Statement 物件,並將其快取起來。因為每一個 SqlSession 都有一個新的 Executor 物件,所以我們快取在 ReuseExecutor 上的 Statement作用域是同一個 SqlSession。

BatchExecutor : 批處理執行器,用於將多個 SQL 一次性輸出到資料庫

CachingExecutor: 快取執行器,先從快取中查詢結果,如果存在就返回之前的結果;如果不存在,再委託給Executor delegate 去資料庫中取,delegate 可以是上面任何一個執行器。

Executor 的建立和選擇

我們上面提到 Executor 是由 Configuration 建立的,Configuration 會根據執行器的型別建立,如下

這一步就是執行器的建立過程,根據傳入的 ExecutorType 型別來判斷是哪種執行器,如果不指定 ExecutorType ,預設建立的是簡單執行器。它的賦值可以通過兩個地方進行賦值:

  • 可以通過<settings>標籤來設定當前工程中所有的 SqlSession 物件使用預設的 Executor
<settings>
 <!--取值範圍 SIMPLE, REUSE, BATCH -->
	<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
  • 另外一種直接通過Java對方法賦值的方式
session = factory.openSession(ExecutorType.BATCH);

Executor 的具體執行過程

Executor 中的大部分方法的呼叫鏈其實是差不多的,下面是深入原始碼分析執行過程,如果你沒有時間或者暫時不想深入研究的話,給你下面的執行流程圖作為參考。

我們緊跟著上面的 selectList 繼續分析,它會呼叫到 executor.query 方法。

當有一個查詢請求訪問的時候,首先會經過 Executor 的實現類 CachingExecutor ,先從快取中查詢 SQL 是否是第一次執行,如果是第一次執行的話,那麼就直接執行 SQL 語句,並建立快取,如果第二次訪問相同的 SQL 語句的話,那麼就會直接從快取中提取。

上面這段程式碼是從 selectList -> 從快取中 query 的具體過程。可能你看到這裡有些覺得類都是什麼東西,我想鼓勵你一下,把握重點,不用每段程式碼都看,從找到 SQL 的呼叫鏈路,其他程式碼想看的時候在看,看原始碼就是很容易發矇,容易煩躁,但是切記一點,把握重點。

上面程式碼會判斷快取中是否有這條 SQL 語句的執行結果,如果沒有的話,就再重新建立 Executor 執行器執行 SQL 語句,注意, list = doQuery 是真正執行 SQL 語句的過程,這個過程中會建立我們上面提到的三種執行器,這裡我們使用的是簡單執行器。

到這裡,執行器所做的工作就完事了,Executor 會把後續的工作交給 StatementHandler 繼續執行。下面我們來認識一下 StatementHandler

StatementHandler

StatementHandler 是四大元件中最重要的一個物件,負責操作 Statement 物件與資料庫進行互動,在工作時還會使用 ParameterHandlerResultSetHandler對引數進行對映,對結果進行實體類的繫結,這兩個元件我們後面說。

我們在搭建原生 JDBC 的時候,會有這樣一行程式碼

Statement stmt = conn.createStatement(); //也可以使用PreparedStatement來做

這行程式碼建立的 Statement 物件或者是 PreparedStatement 物件就是由 StatementHandler 進行管理的。

StatementHandler 的繼承結構

有沒有感覺和 Executor 的繼承體系很相似呢?最頂級介面是四大元件物件,分別有兩個實現類 BaseStatementHandlerRoutingStatementHandler,BaseStatementHandler 有三個實現類, 他們分別是 SimpleStatementHandler、PreparedStatementHandler 和 CallableStatementHandler。

RoutingStatementHandler : RoutingStatementHandler 並沒有對 Statement 物件進行使用,只是根據StatementType 來建立一個代理,代理的就是對應Handler的三種實現類。在MyBatis工作時,使用的StatementHandler 介面物件實際上就是 RoutingStatementHandler 物件。

BaseStatementHandler : 是 StatementHandler 介面的另一個實現類,它本身是一個抽象類,用於簡化StatementHandler 介面實現的難度,屬於介面卡設計模式體現,它主要有三個實現類

  • SimpleStatementHandler: 管理 Statement 物件並向資料庫中推送不需要預編譯的SQL語句。
  • PreparedStatementHandler: 管理 Statement 物件並向資料中推送需要預編譯的SQL語句。
  • CallableStatementHandler:管理 Statement 物件並呼叫資料庫中的儲存過程。

這裡注意一下,SimpleStatementHandler 和 PreparedStatementHandler 的區別是 SQL 語句是否包含變數,是否通過外部進行引數傳入。

SimpleStatementHandler 用於執行沒有任何引數傳入的 SQL

PreparedStatementHandler 需要對外部傳入的變數和引數進行提前引數繫結和賦值。

StatementHandler 的建立和原始碼分析

我們繼續來分析上面 query 的呼叫鏈路,StatementHandler 的建立過程如下

MyBatis 會根據 SQL 語句的型別進行對應 StatementHandler 的建立。我們以預處理 StatementHandler 為例來講解一下

執行器不僅掌管著 StatementHandler 的建立,還掌管著建立 Statement 物件,設定引數等,在建立完 PreparedStatement 之後,我們需要對引數進行處理了。

如果用一副圖來表示一下這個執行流程的話我想是這樣

這裡我們先暫停一下,來認識一下第三個核心元件 ParameterHandler

ParameterHandler

ParameterHandler 介紹

ParameterHandler 相比於其他的元件就簡單很多了,ParameterHandler 譯為引數處理器,負責為 PreparedStatement 的 sql 語句引數動態賦值,這個介面很簡單隻有兩個方法

ParameterHandler 只有一個實現類 DefaultParameterHandler , 它實現了這兩個方法。

  • getParameterObject: 用於讀取引數
  • setParameters: 用於對 PreparedStatement 的引數賦值

ParameterHandler 的解析過程

上面我們討論過了 ParameterHandler 的建立過程,下面我們繼續上面 parameterSize 流程

這就是具體引數的解析過程了,下面我們來描述一下

public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  // parameterMappings 就是對 #{} 或者 ${} 裡面引數的封裝
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
    // 如果是引數化的SQL,便需要迴圈取出並設定引數的值
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      // 如果引數型別不是 OUT ,這個型別與 CallableStatementHandler 有關
      // 因為儲存過程不存在輸出引數,所以引數不是輸出引數的時候,就需要設定。
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        // 得到 #{}  中的屬性名
        String propertyName = parameterMapping.getProperty();
        // 如果 propertyName 是 Map 中的key
        if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
          // 通過key 來得到 additionalParameter 中的value值
          value = boundSql.getAdditionalParameter(propertyName);
        }
        // 如果不是 additionalParameters 中的key,而且傳入引數是 null, 則value 就是null
        else if (parameterObject == null) {
          value = null;
        }
        // 如果 typeHandlerRegistry 中已經註冊了這個引數的 Class 物件,即它是 Primitive 或者是String 的話
        else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          // 否則就是 Map
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // 在通過 SqlSource 的parse 方法得到parameterMappings 的具體實現中,我們會得到parameterMappings 的 typeHandler
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        // 獲取 typeHandler 的jdbc type
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
        }
        try {
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        } catch (TypeException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        } catch (SQLException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
      }
    }
  }
}

下面用一個流程圖表示一下 ParameterHandler 的解析過程,以簡單執行器為例

我們在完成 ParameterHandler 對 SQL 引數的預處理後,回到 SimpleExecutor 中的 doQuery 方法

上面又引出來了一個重要的元件那就是 ResultSetHandler,下面我們來認識一下這個元件

ResultSetHandler

ResultSetHandler 簡介

ResultSetHandler 也是一個非常簡單的介面

ResultSetHandler 是一個介面,它只有一個預設的實現類,像是 ParameterHandler 一樣,它的預設實現類是DefaultResultSetHandler

ResultSetHandler 解析過程

MyBatis 只有一個預設的實現類就是 DefaultResultSetHandler,DefaultResultSetHandler 主要負責處理兩件事

  • 處理 Statement 執行後產生的結果集,生成結果列表

  • 處理儲存過程執行後的輸出引數

按照 Mapper 檔案中配置的 ResultType 或 ResultMap 來封裝成對應的物件,最後將封裝的物件返回即可。

public List<Object> handleResultSets(Statement stmt) throws SQLException {
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

  final List<Object> multipleResults = new ArrayList<Object>();

  int resultSetCount = 0;
  // 獲取第一個結果集
  ResultSetWrapper rsw = getFirstResultSet(stmt);
  // 獲取結果對映
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  // 結果對映的大小
  int resultMapCount = resultMaps.size();
  // 校驗結果對映的數量
  validateResultMapsCount(rsw, resultMapCount);
  // 如果ResultSet 包裝器不是null, 並且 resultmap 的數量  >  resultSet 的數量的話
  // 因為 resultSetCount 第一次肯定是0,所以直接判斷 ResultSetWrapper 是否為 0 即可
  while (rsw != null && resultMapCount > resultSetCount) {
    // 從 resultMap 中取出 resultSet 數量
    ResultMap resultMap = resultMaps.get(resultSetCount);
    // 處理結果集, 關閉結果集
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

  // 從 mappedStatement 取出結果集
  String[] resultSets = mappedStatement.getResultSets();
  if (resultSets != null) {
    while (rsw != null && resultSetCount < resultSets.length) {
      ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
      if (parentMapping != null) {
        String nestedResultMapId = parentMapping.getNestedResultMapId();
        ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
        handleResultSet(rsw, resultMap, null, parentMapping);
      }
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
  }

  return collapseSingleResultList(multipleResults);
}

其中涉及的主要物件有:

ResultSetWrapper : 結果集的包裝器,主要針對結果集進行的一層包裝,它的主要屬性有

  • ResultSet : Java JDBC ResultSet 介面表示資料庫查詢的結果。 有關查詢的文字顯示瞭如何將查詢結果作為java.sql.ResultSet 返回。 然後迭代此ResultSet以檢查結果。

  • TypeHandlerRegistry: 型別註冊器,TypeHandlerRegistry 在初始化的時候會把所有的 Java型別和型別轉換器進行註冊。

  • ColumnNames: 欄位的名稱,也就是查詢操作需要返回的欄位名稱

  • ClassNames: 欄位的型別名稱,也就是 ColumnNames 每個欄位名稱的型別

  • JdbcTypes: JDBC 的型別,也就是 java.sql.Types 型別

  • ResultMap: 負責處理更復雜的對映關係

在 DefaultResultSetHandler 中處理完結果對映,並把上述結構返回給呼叫的客戶端,從而執行完成一條完整的SQL語句。

文章參考:

MyBatis的優缺點以及特點

mybatis基礎,mybatis核心配置檔案properties元素

https://mybatis.org/mybatis-3/zh/configuration.html#properties

深入淺出Mybatis系列(十)—SQL執行流程分析(原始碼篇)

https://www.jianshu.com/p/19686af69b0d

http://www.mybatis.org/mybatis-3/getting-started.html

https://www.cnblogs.com/virgosnail/p/10067964.html

https://blog.csdn.net/Roger_CoderLife/article/details/88707076

https://blog.csdn.net/qq924862077/article/details/52704191

[mybatis 原始碼分析(八)ResultSetHandler 詳解](

相關文章