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


一、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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM