從原始碼角度分析 MyBatis 工作原理

vivo網際網路技術發表於2021-09-07

一、MyBatis 完整示例

這裡,我將以一個入門級的示例來演示 MyBatis 是如何工作的。

注:本文後面章節中的原理、原始碼部分也將基於這個示例來進行講解。完整示例原始碼地址

1.1. 資料庫準備

在本示例中,需要針對一張使用者表進行 CRUD 操作。其資料模型如下:

CREATE TABLE IF NOT EXISTS user (
    id      BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',
    name    VARCHAR(10)         NOT NULL DEFAULT '' COMMENT '使用者名稱',
    age     INT(3)              NOT NULL DEFAULT 0 COMMENT '年齡',
    address VARCHAR(32)         NOT NULL DEFAULT '' COMMENT '地址',
    email   VARCHAR(32)         NOT NULL DEFAULT '' COMMENT '郵件',
    PRIMARY KEY (id)
) COMMENT = '使用者表';

INSERT INTO user (name, age, address, email)
VALUES ('張三', 18, '北京', 'xxx@163.com');
INSERT INTO user (name, age, address, email)
VALUES ('李四', 19, '上海', 'xxx@163.com');

1.2. 新增 MyBatis

如果使用 Maven 來構建專案,則需將下面的依賴程式碼置於 pom.xml 檔案中:

<dependency>
  <groupId>org.Mybatis</groupId>
  <artifactId>Mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

1.3. MyBatis 配置

XML 配置檔案中包含了對 MyBatis 系統的核心設定,包括獲取資料庫連線例項的資料來源(DataSource)以及決定事務作用域和控制方式的事務管理器(TransactionManager)。

本示例中只是給出最簡化的配置。【示例】MyBatis-config.xml 檔案

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE configuration PUBLIC "-//Mybatis.org//DTD Config 3.0//EN"
  "http://Mybatis.org/dtd/Mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC" />
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver" />
        <property name="url"
                  value="jdbc:mysql://127.0.0.1:3306/spring_tutorial?serverTimezone=UTC" />
        <property name="username" value="root" />
        <property name="password" value="root" />
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="Mybatis/mapper/UserMapper.xml" />
  </mappers>
</configuration>

說明:上面的配置檔案中僅僅指定了資料來源連線方式和 User 表的對映配置檔案。

1.4 Mapper

1.4.1 Mapper.xml

個人理解,Mapper.xml 檔案可以看做是 MyBatis 的 JDBC SQL 模板。【示例】UserMapper.xml 檔案。

下面是一個通過 MyBatis Generator 自動生成的完整的 Mapper 檔案。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//Mybatis.org//DTD Mapper 3.0//EN" "http://Mybatis.org/dtd/Mybatis-3-mapper.dtd">
<mapper namespace="io.github.dunwu.spring.orm.mapper.UserMapper">
  <resultMap id="BaseResultMap" type="io.github.dunwu.spring.orm.entity.User">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="age" jdbcType="INTEGER" property="age" />
    <result column="address" jdbcType="VARCHAR" property="address" />
    <result column="email" jdbcType="VARCHAR" property="email" />
  </resultMap>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
    delete from user
    where id = #{id,jdbcType=BIGINT}
  </delete>
  <insert id="insert" parameterType="io.github.dunwu.spring.orm.entity.User">
    insert into user (id, name, age,
      address, email)
    values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER},
      #{address,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR})
  </insert>
  <update id="updateByPrimaryKey" parameterType="io.github.dunwu.spring.orm.entity.User">
    update user
    set name = #{name,jdbcType=VARCHAR},
      age = #{age,jdbcType=INTEGER},
      address = #{address,jdbcType=VARCHAR},
      email = #{email,jdbcType=VARCHAR}
    where id = #{id,jdbcType=BIGINT}
  </update>
  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    select id, name, age, address, email
    from user
    where id = #{id,jdbcType=BIGINT}
  </select>
  <select id="selectAll" resultMap="BaseResultMap">
    select id, name, age, address, email
    from user
  </select>
</mapper>

1.4.2 Mapper.java

Mapper.java 檔案是 Mapper.xml 對應的 Java 物件。【示例】UserMapper.java 檔案

public interface UserMapper {

    int deleteByPrimaryKey(Long id);

    int insert(User record);

    User selectByPrimaryKey(Long id);

    List<User> selectAll();

    int updateByPrimaryKey(User record);

}

對比 UserMapper.java 和 UserMapper.xml 檔案,不難發現:UserMapper.java 中的方法和 UserMapper.xml 的 CRUD 語句元素(  的 parameterType 屬性以及  的 type 屬性都可能會繫結到資料實體。這樣就可以把 JDBC 操作的輸入輸出和 JavaBean 結合起來,更加方便、易於理解。

1.5. 測試程式

【示例】MyBatisDemo.java 檔案

public class MyBatisDemo {

    public static void main(String[] args) throws Exception {
        // 1. 載入 MyBatis 配置檔案,建立 SqlSessionFactory
        // 注:在實際的應用中,SqlSessionFactory 應該是單例
        InputStream inputStream = Resources.getResourceAsStream("MyBatis/MyBatis-config.xml");
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(inputStream);

        // 2. 建立一個 SqlSession 例項,進行資料庫操作
        SqlSession sqlSession = factory.openSession();

        // 3. Mapper 對映並執行
        Long params = 1L;
        List<User> list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params);
        for (User user : list) {
            System.out.println("user name: " + user.getName());
        }
        // 輸出:user name: 張三
    }

}

說明:SqlSession 介面是 MyBatis API 核心中的核心,它代表 MyBatis 和資料庫一次完整會話。

  • MyBatis 會解析配置,並根據配置建立 SqlSession 。

  • 然後,MyBatis 將 Mapper 對映為 SqlSession,然後傳遞引數,執行 SQL 語句並獲取結果。

二、MyBatis 生命週期

2.1. SqlSessionFactoryBuilder

2.1.1 SqlSessionFactoryBuilder 的職責

SqlSessionFactoryBuilder 負責建立 SqlSessionFactory 例項。

SqlSessionFactoryBuilder 可以從 XML 配置檔案或一個預先定製的 Configuration 的例項構建出 SqlSessionFactory 的例項。

Configuration 類包含了對一個 SqlSessionFactory 例項你可能關心的所有內容。

SqlSessionFactoryBuilder 應用了建造者設計模式,它有五個 build 方法,允許你通過不同的資源建立 SqlSessionFactory 例項。

SqlSessionFactory build(InputStream inputStream)
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)

2.1.2 SqlSessionFactoryBuilder 的生命週期

SqlSessionFactoryBuilder 可以被例項化、使用和丟棄,一旦建立了 SqlSessionFactory,就不再需要它了。因此 SqlSessionFactoryBuilder 例項的最佳作用域是方法作用域(也就是區域性方法變數)。

你可以重用 SqlSessionFactoryBuilder 來建立多個 SqlSessionFactory 例項,但最好還是不要一直保留著它,以保證所有的 XML 解析資源可以被釋放給更重要的事情。

2.2. SqlSessionFactory

2.2.1 SqlSessionFactory 職責

SqlSessionFactory 負責建立 SqlSession 例項。

SqlSessionFactory 應用了工廠設計模式,它提供了一組方法,用於建立 SqlSession 例項。

SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
Configuration getConfiguration();

方法說明:

預設的 openSession() 方法沒有引數,它會建立具備如下特性的 SqlSession:

1)事務作用域將會開啟(也就是不自動提交)。

  • 將由當前環境配置的 DataSource 例項中獲取 Connection 物件。

  • 事務隔離級別將會使用驅動或資料來源的預設設定。

  • 預處理語句不會被複用,也不會批量處理更新。

2)TransactionIsolationLevel 表示事務隔離級別,它對應著 JDBC 的五個事務隔離級別。

3)ExecutorType 列舉型別定義了三個值:

  • ExecutorType.SIMPLE:該型別的執行器沒有特別的行為。它為每個語句的執行建立一個新的預處理語句。

  • ExecutorType.REUSE:該型別的執行器會複用預處理語句。

  • ExecutorType.BATCH:該型別的執行器會批量執行所有更新語句,如果 SELECT 在多個更新中間執行,將在必要時將多條更新語句分隔開來,以方便理解。

2.2.2 SqlSessionFactory 生命週期

SQLSessionFactory 應該以單例形式在應用的執行期間一直存在。

2.3. SqlSession

2.3.1 SqlSession 職責

MyBatis 的主要 Java 介面就是 SqlSession。它包含了所有執行語句,獲取對映器和管理事務等方法。詳細內容可以參考:「 MyBatis 官方文件之 SqlSessions 」 。

SQLSession 類的方法可按照下圖進行大致分類:

2.3.2 SqlSession 生命週期

SqlSessions 是由 SqlSessionFactory 例項建立的;而 SqlSessionFactory 是由 SqlSessionFactoryBuilder 建立的。

注意:當 MyBatis 與一些依賴注入框架(如 Spring 或者 Guice)同時使用時,SqlSessions 將被依賴注入框架所建立,所以你不需要使用 SqlSessionFactoryBuilder 或者 SqlSessionFactory。

每個執行緒都應該有它自己的 SqlSession 例項。

SqlSession 的例項不是執行緒安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。絕對不能將 SqlSession 例項的引用放在一個類的靜態域,甚至一個類的例項變數也不行。也絕不能將 SqlSession 例項的引用放在任何型別的託管作用域中,比如 Servlet 框架中的 HttpSession。正確在 Web 中使用 SqlSession 的場景是:每次收到的 HTTP 請求,就可以開啟一個 SqlSession,返回一個響應,就關閉它。

程式設計模式:

try (SqlSession session = sqlSessionFactory.openSession()) {  // 你的應用邏輯程式碼}

2.4. 對映器

2.4.1 對映器職責

對映器是一些由使用者建立的、繫結 SQL 語句的介面。

SqlSession 中的 insert、update、delete 和 select 方法都很強大,但也有些繁瑣。更通用的方式是使用對映器類來執行對映語句。一個對映器類就是一個僅需宣告與 SqlSession 方法相匹配的方法的介面類。

MyBatis 將配置檔案中的每一個 節點抽象為一個 Mapper 介面,而這個介面中宣告的方法和跟  節點中的 <select|update|delete|insert> 節點相對應,即 <select|update|delete|insert> 節點的 id 值為 Mapper 介面中的方法名稱,parameterType 值表示 Mapper 對應方法的入參型別,而 resultMap 值則對應了 Mapper 介面表示的返回值型別或者返回結果集的元素型別。

MyBatis 會根據相應的介面宣告的方法資訊,通過動態代理機制生成一個 Mapper 例項;MyBatis 會根據這個方法的方法名和引數型別,確定 Statement id,然後和 SqlSession 進行對映,底層還是通過 SqlSession 完成和資料庫的互動。

下面的示例展示了一些方法簽名以及它們是如何對映到 SqlSession 上的。

注意:

  • 對映器介面不需要去實現任何介面或繼承自任何類。只要方法可以被唯一標識對應的對映語句就可以了。

  • 對映器介面可以繼承自其他介面。當使用 XML 來構建對映器介面時要保證語句被包含在合適的名稱空間中。而且,唯一的限制就是你不能在兩個繼承關係的介面中擁有相同的方法簽名(潛在的危險做法不可取)。

2.4.2 對映器生命週期

對映器介面的例項是從 SqlSession 中獲得的。因此從技術層面講,任何對映器例項的最大作用域是和請求它們的 SqlSession 相同的。儘管如此,對映器例項的最佳作用域是方法作用域。也就是說,對映器例項應該在呼叫它們的方法中被請求,用過之後即可丟棄。

程式設計模式:

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 你的應用邏輯程式碼
}

對映器註解

MyBatis 是一個 XML 驅動的框架。配置資訊是基於 XML 的,而且對映語句也是定義在 XML 中的。MyBatis 3 以後,支援註解配置。註解配置基於配置 API;而配置 API 基於 XML 配置。

MyBatis 支援諸如 @Insert、@Update、@Delete、@Select、@Result 等註解。

詳細內容請參考:MyBatis 官方文件之 sqlSessions,其中列舉了 MyBatis 支援的註解清單,以及基本用法。

三、 MyBatis 的架構

從 MyBatis 程式碼實現的角度來看,MyBatis 的主要元件有以下幾個:

  • SqlSession - 作為 MyBatis 工作的主要頂層 API,表示和資料庫互動的會話,完成必要資料庫增刪改查功能。

  • Executor - MyBatis 執行器,是 MyBatis 排程的核心,負責 SQL 語句的生成和查詢快取的維護。

  • StatementHandler - 封裝了 JDBC Statement 操作,負責對 JDBC statement 的操作,如設定引數、將 Statement 結果集轉換成 List 集合。

  • ParameterHandler - 負責對使用者傳遞的引數轉換成 JDBC Statement 所需要的引數。

  • ResultSetHandler - 負責將 JDBC 返回的 ResultSet 結果集物件轉換成 List 型別的集合。

  • TypeHandler - 負責 java 資料型別和 jdbc 資料型別之間的對映和轉換。

  • MappedStatement - MappedStatement 維護了一條 <select|update|delete|insert> 節點的封裝。

  • SqlSource - 負責根據使用者傳遞的 parameterObject,動態地生成 SQL 語句,將資訊封裝到 BoundSql 物件中,並返回。

  • BoundSql - 表示動態生成的 SQL 語句以及相應的引數資訊。

  • Configuration - MyBatis 所有的配置資訊都維持在 Configuration 物件之中。

這些元件的架構層次如下:

3.1. 配置層

配置層決定了 MyBatis 的工作方式。

MyBatis 提供了兩種配置方式:

  • 基於 XML 配置檔案的方式

  • 基於 Java API 的方式

SqlSessionFactoryBuilder 會根據配置建立 SqlSessionFactory ;

SqlSessionFactory 負責建立 SqlSessions 。

3.2. 介面層

介面層負責和資料庫互動的方式。MyBatis 和資料庫的互動有兩種方式:

1)使用 SqlSession:SqlSession 封裝了所有執行語句,獲取對映器和管理事務的方法。

  • 使用者只需要傳入 Statement Id 和查詢引數給 SqlSession 物件,就可以很方便的和資料庫進行互動。

  • 這種方式的缺點是不符合物件導向程式設計的正規化。

2)使用 Mapper 介面:MyBatis 會根據相應的介面宣告的方法資訊,通過動態代理機制生成一個 Mapper 例項;MyBatis 會根據這個方法的方法名和引數型別,確定 Statement Id,然後和 SqlSession 進行對映,底層還是通過 SqlSession 完成和資料庫的互動。

3.3. 資料處理層

資料處理層可以說是 MyBatis 的核心,從大的方面上講,它要完成兩個功能:

1)根據傳參 Statement 和引數構建動態 SQL 語句

  • 動態語句生成可以說是 MyBatis 框架非常優雅的一個設計,MyBatis 通過傳入的引數值,使用 Ognl 來動態地構造 SQL 語句,使得 MyBatis 有很強的靈活性和擴充套件性。

  • 引數對映指的是對於 java 資料型別和 jdbc 資料型別之間的轉換:這裡有包括兩個過程:查詢階段,我們要將 java 型別的資料,轉換成 jdbc 型別的資料,通過 preparedStatement.setXXX() 來設值;另一個就是對 resultset 查詢結果集的 jdbcType 資料轉換成 java 資料型別。

2)執行 SQL 語句以及處理響應結果集 ResultSet

  • 動態 SQL 語句生成之後,MyBatis 將執行 SQL 語句,並將可能返回的結果集轉換成 List 列表。

  • MyBatis 在對結果集的處理中,支援結果集關係一對多和多對一的轉換,並且有兩種支援方式,一種為巢狀查詢語句的查詢,還有一種是巢狀結果集的查詢。

3.4. 框架支撐層

  1. 事務管理機制 - MyBatis 將事務抽象成了 Transaction 介面。MyBatis 的事務管理分為兩種形式:
  • 使用 JDBC 的事務管理機制:即利用 java.sql.Connection 物件完成對事務的提交(commit)、回滾(rollback)、關閉(close)等。

  • 使用 MANAGED 的事務管理機制:MyBatis 自身不會去實現事務管理,而是讓程式的容器如(JBOSS,Weblogic)來實現對事務的管理。

  1. 連線池管理

  2. SQL 語句的配置 - 支援兩種方式:

  • xml 配置

  • 註解配置

  1. 快取機制 - MyBatis 採用兩級快取結構;
  • 一級快取是 Session 會話級別的快取 - 一級快取又被稱之為本地快取。一般而言,一個 SqlSession 物件會使用一個 Executor 物件來完成會話操作,Executor 物件會維護一個 Cache 快取,以提高查詢效能。
  1. 一級快取的生命週期是 Session 會話級別的。
  • 二級快取是 Application 應用級別的快取 - 使用者配置了 "cacheEnabled=true",才會開啟二級快取。
  1. 如果開啟了二級快取,SqlSession 會先使用 CachingExecutor 物件來處理查詢請求。CachingExecutor 會在二級快取中檢視是否有匹配的資料,如果匹配,則直接返回快取結果;如果快取中沒有,再交給真正的 Executor 物件來完成查詢,之後 CachingExecutor 會將真正 Executor 返回的查詢結果放置到快取中,然後在返回給使用者。

  2. 二級快取的生命週期是應用級別的。

四、SqlSession 內部工作機制

從前文,我們已經瞭解了,MyBatis 封裝了對資料庫的訪問,把對資料庫的會話和事務控制放到了 SqlSession 物件中。那麼具體是如何工作的呢?接下來,我們通過原始碼解讀來進行分析。

SqlSession 對於 insert、update、delete、select 的內部處理機制基本上大同小異。所以,接下來,我會以一次完整的 select 查詢流程為例講解 SqlSession 內部的工作機制。相信讀者如果理解了 select 的處理流程,對於其他 CRUD 操作也能做到一通百通。

4.1  SqlSession 子元件

前面的內容已經介紹了:SqlSession 是 MyBatis 的頂層介面,它提供了所有執行語句,獲取對映器和管理事務等方法。

實際上,SqlSession 是通過聚合多個子元件,讓每個子元件負責各自功能的方式,實現了任務的下發。

在瞭解各個子元件工作機制前,先讓我們簡單認識一下 SqlSession 的核心子元件。

4.1.1 Executor

Executor 即執行器,它負責生成動態 SQL 以及管理快取。

  • Executor 即執行器介面。

  • BaseExecutor

    是 Executor 的抽象類,它採用了模板方法設計模式,內建了一些共性方法,而將定製化方法留給子類去實現。

  • SimpleExecutor

    是最簡單的執行器。它只會直接執行 SQL,不會做額外的事。

  • BatchExecutor

    是批處理執行器。它的作用是通過批處理來優化效能。值得注意的是,批量更新操作,由於內部有快取機制,使用完後需要呼叫 flushStatements 來清除快取。

  • ReuseExecutor

    是可重用的執行器。重用的物件是 Statement,也就是說,該執行器會快取同一個 SQL 的 Statement,避免重複建立 Statement。其內部的實現是通過一個 HashMap 來維護 Statement 物件的。由於當前 Map 只在該 session 中有效,所以使用完後需要呼叫 flushStatements 來清除 Map。

  • CachingExecutor 是快取執行器。它只在啟用二級快取時才會用到。

4.1.2 StatementHandler

StatementHandler 物件負責設定 Statement 物件中的查詢引數、處理 JDBC 返回的 resultSet,將 resultSet 加工為 List 集合返回。

StatementHandler 的家族成員:

  • StatementHandler 是介面;

  • BaseStatementHandler是實現 StatementHandler 的抽象類,內建一些共性方法;

  • SimpleStatementHandler負責處理 Statement;

  • PreparedStatementHandler負責處理 PreparedStatement;

  • CallableStatementHandler負責處理 CallableStatement。

  • RoutingStatementHandler負責代理 StatementHandler 具體子類,根據 Statement 型別,選擇例項化 SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler。

4.1.3 ParameterHandler

ParameterHandler 負責將傳入的 Java 物件轉換 JDBC 型別物件,併為 PreparedStatement 的動態 SQL 填充數值。

ParameterHandler 只有一個具體實現類,即 DefaultParameterHandler。

4.1.4 ResultSetHandler

ResultSetHandler 負責兩件事:

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

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

ResultSetHandler 只有一個具體實現類,即 DefaultResultSetHandler。

4.1.5 TypeHandler

TypeHandler 負責將 Java 物件型別和 JDBC 型別進行相互轉換。

4.2  SqlSession 和 Mapper

先來回憶一下 MyBatis 完整示例章節的 測試程式部分的程式碼。

MyBatisDemo.java 檔案中的程式碼片段:

// 2. 建立一個 SqlSession 例項,進行資料庫操作
SqlSession sqlSession = factory.openSession();

// 3. Mapper 對映並執行
Long params = 1L;
List<User> list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params);
for (User user : list) {
    System.out.println("user name: " + user.getName());
}

示例程式碼中,給 sqlSession 物件的傳遞一個配置的 Sql 語句的 Statement Id 和引數,然後返回結果io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey 是配置在 UserMapper.xml 的 Statement ID,params 是 SQL 引數。

UserMapper.xml 檔案中的程式碼片段:

 <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    select id, name, age, address, email
    from user
    where id = #{id,jdbcType=BIGINT}
  </select>

MyBatis 通過方法的全限定名,將 SqlSession 和 Mapper 相互對映起來。

4.3. SqlSession 和 Executor

org.apache.ibatis.session.defaults.DefaultSqlSession 中 selectList 方法的原始碼:

@Override
public <E> List<E> selectList(String statement) {
  return this.selectList(statement, null);
}

@Override
public <E> List<E> selectList(String statement, Object parameter) {
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    // 1. 根據 Statement Id,在配置物件 Configuration 中查詢和配置檔案相對應的 MappedStatement
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 2. 將 SQL 語句交由執行器 Executor 處理
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

說明:

MyBatis 所有的配置資訊都維持在 Configuration 物件之中。中維護了一個 Map<String, MappedStatement> 物件。其中,key 為 Mapper 方法的全限定名(對於本例而言,key 就是 io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey ),value 為 MappedStatement 物件。所以,傳入 Statement Id 就可以從 Map 中找到對應的 MappedStatement。

MappedStatement 維護了一個 Mapper 方法的後設資料資訊,資料組織可以參考下面 debug 截圖:

小結:通過 "SqlSession 和 Mapper" 以及 "SqlSession 和 Executor" 這兩節,我們已經知道:SqlSession 的職能是:根據 Statement ID, 在 Configuration 中獲取到對應的 MappedStatement 物件,然後呼叫 Executor 來執行具體的操作。

4.4. Executor 工作流程

繼續上一節的流程,SqlSession 將 SQL 語句交由執行器 Executor 處理。那又做了哪些事呢?

(1)執行器查詢入口

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
	// 1. 根據傳參,動態生成需要執行的 SQL 語句,用 BoundSql 物件表示
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 2. 根據傳參,建立一個快取Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

執行器查詢入口主要做兩件事:

  • 生成動態 SQL:根據傳參,動態生成需要執行的 SQL 語句,用 BoundSql 物件表示。

  • 管理快取:根據傳參,建立一個快取 Key。

(2)執行器查詢第二入口

@SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 略
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      // 3. 快取中有值,則直接從快取中取資料;否則,查詢資料庫
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    // 略
    return list;
  }

實際查詢方法主要的職能是判斷快取 key 是否能命中快取:

  • 命中,則將快取中資料返回;

  • 不命中,則查詢資料庫:

(3)查詢資料庫

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 4. 執行查詢,獲取 List 結果,並將查詢的結果更新本地快取中
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

queryFromDatabase 方法的職責是呼叫 doQuery,向資料庫發起查詢,並將返回的結果更新到本地快取。

(4)實際查詢方法。SimpleExecutor 類的 doQuery()方法實現;

   @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // 5. 根據既有的引數,建立StatementHandler物件來執行查詢操作
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 6. 建立java.Sql.Statement物件,傳遞給StatementHandler物件
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 7. 呼叫StatementHandler.query()方法,返回List結果
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

上述的 Executor.query()方法幾經轉折,最後會建立一個 StatementHandler 物件,然後將必要的引數傳遞給 StatementHandler,使用 StatementHandler 來完成對資料庫的查詢,最終返回 List 結果集。從上面的程式碼中我們可以看出,Executor 的功能和作用是:

  • 根據傳遞的引數,完成 SQL 語句的動態解析,生成 BoundSql 物件,供 StatementHandler 使用;

  • 為查詢建立快取,以提高效能

  • 建立 JDBC 的 Statement 連線物件,傳遞給 StatementHandler 物件,返回 List 查詢結果。

prepareStatement() 方法的實現:

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    //對建立的Statement物件設定引數,即設定SQL 語句中 ? 設定為指定的引數
    handler.parameterize(stmt);
    return stmt;
  }

對於 JDBC 的 PreparedStatement 型別的物件,建立的過程中,我們使用的是 SQL 語句字串會包含若干個佔位符,我們其後再對佔位符進行設值。

4.5. StatementHandler 工作流程

StatementHandler 有一個子類 RoutingStatementHandler,它負責代理其他 StatementHandler 子類的工作。

它會根據配置的 Statement 型別,選擇例項化相應的 StatementHandler,然後由其代理物件完成工作。

【原始碼】RoutingStatementHandler

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

  switch (ms.getStatementType()) {
    case STATEMENT:
      delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case PREPARED:
      delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case CALLABLE:
      delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    default:
      throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
  }

}

【原始碼】RoutingStatementHandler 的 parameterize 方法原始碼

【原始碼】PreparedStatementHandler 的 parameterize 方法原始碼

StatementHandler使用ParameterHandler物件來完成對Statement 的賦值。

@Override
public void parameterize(Statement statement) throws SQLException {
  // 使用 ParameterHandler 物件來完成對 Statement 的設值
  parameterHandler.setParameters((PreparedStatement) statement);
}

【原始碼】StatementHandler 的 query 方法原始碼

StatementHandler 使用 ResultSetHandler 物件來完成對 ResultSet 的處理。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  ps.execute();
  // 使用ResultHandler來處理ResultSet
  return resultSetHandler.handleResultSets(ps);
}

4.6. ParameterHandler 工作流程

【原始碼】DefaultParameterHandler 的 setParameters 方法

@Override
  public void setParameters(PreparedStatement ps) {
	// parameterMappings 是對佔位符 #{} 對應引數的封裝
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        // 不處理儲存過程中的引數
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            // 獲取對應的實際數值
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            // 獲取物件中相應的屬性或查詢 Map 物件中的值
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }

          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            // 通過 TypeHandler 將 Java 物件引數轉為 JDBC 型別的引數
            // 然後,將數值動態繫結到 PreparedStaement 中
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

4.7. ResultSetHandler 工作流程

ResultSetHandler 的實現可以概括為:將 Statement 執行後的結果集,按照 Mapper 檔案中配置的 ResultType 或 ResultMap 來轉換成對應的 JavaBean 物件,最後將結果返回。

【原始碼】DefaultResultSetHandler 的 handleResultSets 方法。handleResultSets 方法是 DefaultResultSetHandler 的最關鍵方法。其實現如下:

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

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

  int resultSetCount = 0;
  // 第一個結果集
  ResultSetWrapper rsw = getFirstResultSet(stmt);
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  // 判斷結果集的數量
  int resultMapCount = resultMaps.size();
  validateResultMapsCount(rsw, resultMapCount);
  // 遍歷處理結果集
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

  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);
}

五、參考資料

官方

  1. MyBatis Github

  2. MyBatis 官網

  3. MyBatis Generator

  4. Spring 整合

  5. Spring Boot 整合

擴充套件外掛

  1. MyBatis-plus - CRUD 擴充套件外掛、程式碼生成器、分頁器等多功能

  2. Mapper - CRUD 擴充套件外掛

  3. MyBatis-PageHelper - MyBatis 通用分頁外掛

文章

  1. 深入理解 MyBatis 原理

  2. MyBatis 原始碼中文註釋

  3. MyBatis 中強大的 resultMap

作者:vivo網際網路伺服器團隊-Zhang Peng

相關文章