天天看點

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

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

注:本文後面章節中的原理、源碼部分也将基于這個示例來進行講解。完整示例源碼位址

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

如果使用 Maven 來建構項目,則需将下面的依賴代碼置于 pom.xml 檔案中:

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

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

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

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

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

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

對比 UserMapper.java 和 UserMapper.xml 檔案,不難發現:UserMapper.java 中的方法和 UserMapper.xml 的 CRUD 語句元素( <insert>、<delete>、<update>、<select>)存在一一對應關系。

在 MyBatis 中,正是通過方法的全限定名,将二者綁定在一起。

【示例】User.java 檔案

<insert>、<delete>、<update>、<select> 的 parameterType 屬性以及 <resultMap> 的 type 屬性都可能會綁定到資料實體。這樣就可以把 JDBC 操作的輸入輸出和 JavaBean 結合起來,更加友善、易于了解。

【示例】MyBatisDemo.java 檔案

說明:SqlSession 接口是 MyBatis API 核心中的核心,它代表 MyBatis 和資料庫一次完整會話。 MyBatis 會解析配置,并根據配置建立 SqlSession 。 然後,MyBatis 将 Mapper 映射為 SqlSession,然後傳遞參數,執行 SQL 語句并擷取結果。
從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

SqlSessionFactoryBuilder 負責建立 SqlSessionFactory 執行個體。

SqlSessionFactoryBuilder 可以從 XML 配置檔案或一個預先定制的 Configuration 的執行個體建構出 SqlSessionFactory 的執行個體。

Configuration 類包含了對一個 SqlSessionFactory 執行個體你可能關心的所有内容。

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

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

SqlSessionFactoryBuilder 可以被執行個體化、使用和丢棄,一旦建立了 SqlSessionFactory,就不再需要它了。是以 SqlSessionFactoryBuilder 執行個體的最佳作用域是方法作用域(也就是局部方法變量)。

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

SqlSessionFactory 負責建立 SqlSession 執行個體。

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

SqlSessionFactory 應用了工廠設計模式,它提供了一組方法,用于建立 SqlSession 執行個體。

方法說明:

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

1)事務作用域将會開啟(也就是不自動送出)。

将由目前環境配置的 DataSource 執行個體中擷取 Connection 對象。

事務隔離級别将會使用驅動或資料源的預設設定。

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

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

3)ExecutorType 枚舉類型定義了三個值:

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

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

ExecutorType.BATCH:該類型的執行器會批量執行所有更新語句,如果 SELECT 在多個更新中間執行,将在必要時将多條更新語句分隔開來,以友善了解。

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

MyBatis 的主要 Java 接口就是 SqlSession。它包含了所有執行語句,擷取映射器和管理事務等方法。詳細内容可以參考:「 MyBatis 官方文檔之 SqlSessions 」 。

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

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

SqlSessions 是由 SqlSessionFactory 執行個體建立的;而 SqlSessionFactory 是由 SqlSessionFactoryBuilder 建立的。

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

每個線程都應該有它自己的 SqlSession 執行個體。

SqlSession 的執行個體不是線程安全的,是以是不能被共享的,是以它的最佳的作用域是請求或方法作用域。絕對不能将 SqlSession 執行個體的引用放在一個類的靜态域,甚至一個類的執行個體變量也不行。也絕不能将 SqlSession 執行個體的引用放在任何類型的托管作用域中,比如 Servlet 架構中的 HttpSession。正确在 Web 中使用 SqlSession 的場景是:每次收到的 HTTP 請求,就可以打開一個 SqlSession,傳回一個響應,就關閉它。

程式設計模式:

映射器是一些由使用者建立的、綁定 SQL 語句的接口。

SqlSession 中的 insert、update、delete 和 select 方法都很強大,但也有些繁瑣。更通用的方式是使用映射器類來執行映射語句。一個映射器類就是一個僅需聲明與 SqlSession 方法相比對的方法的接口類。

MyBatis 将配置檔案中的每一個 <mapper> 節點抽象為一個 Mapper 接口,而這個接口中聲明的方法和跟 <mapper> 節點中的 <select|update|delete|insert> 節點相對應,即 <select|update|delete|insert> 節點的 id 值為 Mapper 接口中的方法名稱,parameterType 值表示 Mapper 對應方法的入參類型,而 resultMap 值則對應了 Mapper 接口表示的傳回值類型或者傳回結果集的元素類型。

MyBatis 會根據相應的接口聲明的方法資訊,通過動态代理機制生成一個 Mapper 執行個體;MyBatis 會根據這個方法的方法名和參數類型,确定 Statement id,然後和 SqlSession 進行映射,底層還是通過 SqlSession 完成和資料庫的互動。

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

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

注意:

映射器接口不需要去實作任何接口或繼承自任何類。隻要方法可以被唯一辨別對應的映射語句就可以了。

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

映射器接口的執行個體是從 SqlSession 中獲得的。是以從技術層面講,任何映射器執行個體的最大作用域是和請求它們的 SqlSession 相同的。盡管如此,映射器執行個體的最佳作用域是方法作用域。也就是說,映射器執行個體應該在調用它們的方法中被請求,用過之後即可丢棄。

映射器注解

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

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

詳細内容請參考:MyBatis 官方文檔之 sqlSessions,其中列舉了 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 對象之中。

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

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

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

MyBatis 提供了兩種配置方式:

基于 XML 配置檔案的方式

基于 Java API 的方式

SqlSessionFactoryBuilder 會根據配置建立 SqlSessionFactory ;

SqlSessionFactory 負責建立 SqlSessions 。

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

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

使用者隻需要傳入 Statement Id 和查詢參數給 SqlSession 對象,就可以很友善的和資料庫進行互動。

這種方式的缺點是不符合面向對象程式設計的範式。

2)使用 Mapper 接口:MyBatis 會根據相應的接口聲明的方法資訊,通過動态代理機制生成一個 Mapper 執行個體;MyBatis 會根據這個方法的方法名和參數類型,确定 Statement Id,然後和 SqlSession 進行映射,底層還是通過 SqlSession 完成和資料庫的互動。

資料處理層可以說是 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<E> 清單。

MyBatis 在對結果集的進行中,支援結果集關系一對多和多對一的轉換,并且有兩種支援方式,一種為嵌套查詢語句的查詢,還有一種是嵌套結果集的查詢。

1) 事務管理機制 - MyBatis 将事務抽象成了 Transaction 接口。MyBatis 的事務管理分為兩種形式:

使用 JDBC 的事務管理機制:即利用 java.sql.Connection 對象完成對事務的送出(commit)、復原(rollback)、關閉(close)等。

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

2) 連接配接池管理

3) SQL 語句的配置 - 支援兩種方式:

xml 配置

注解配置

4) 緩存機制 - MyBatis 采用兩級緩存結構;

一級緩存是 Session 會話級别的緩存 - 一級緩存又被稱之為本地緩存。一般而言,一個 SqlSession 對象會使用一個 Executor 對象來完成會話操作,Executor 對象會維護一個 Cache 緩存,以提高查詢性能。

一級緩存的生命周期是 Session 會話級别的。

二級緩存是 Application 應用級别的緩存 - 使用者配置了 "cacheEnabled=true",才會開啟二級緩存。

如果開啟了二級緩存,SqlSession 會先使用 CachingExecutor 對象來處理查詢請求。CachingExecutor 會在二級緩存中檢視是否有比對的資料,如果比對,則直接傳回緩存結果;如果緩存中沒有,再交給真正的 Executor 對象來完成查詢,之後 CachingExecutor 會将真正 Executor 傳回的查詢結果放置到緩存中,然後在傳回給使用者。

二級緩存的生命周期是應用級别的。

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

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

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

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

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

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

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

Executor 即執行器,它負責生成動态 SQL 以及管理緩存。

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

Executor 即執行器接口。

BaseExecutor

是 Executor 的抽象類,它采用了模闆方法設計模式,内置了一些共性方法,而将定制化方法留給子類去實作。

SimpleExecutor

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

BatchExecutor

是批處理執行器。它的作用是通過批處理來優化性能。值得注意的是,批量更新操作,由于内部有緩存機制,使用完後需要調用 flushStatements 來清除緩存。

ReuseExecutor

是可重用的執行器。重用的對象是 Statement,也就是說,該執行器會緩存同一個 SQL 的 Statement,避免重複建立 Statement。其内部的實作是通過一個 HashMap 來維護 Statement 對象的。由于目前 Map 隻在該 session 中有效,是以使用完後需要調用 flushStatements 來清除 Map。

CachingExecutor 是緩存執行器。它隻在啟用二級緩存時才會用到。

StatementHandler 對象負責設定 Statement 對象中的查詢參數、處理 JDBC 傳回的 resultSet,将 resultSet 加工為 List 集合傳回。

StatementHandler 的家族成員:

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料

StatementHandler 是接口;

BaseStatementHandler是實作 StatementHandler 的抽象類,内置一些共性方法;

SimpleStatementHandler負責處理 Statement;

PreparedStatementHandler負責處理 PreparedStatement;

CallableStatementHandler負責處理 CallableStatement。

RoutingStatementHandler負責代理 StatementHandler 具體子類,根據 Statement 類型,選擇執行個體化 SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler。

ParameterHandler 負責将傳入的 Java 對象轉換 JDBC 類型對象,并為 PreparedStatement 的動态 SQL 填充數值。

ParameterHandler 隻有一個具體實作類,即 DefaultParameterHandler。

ResultSetHandler 負責兩件事:

處理 Statement 執行後産生的結果集,生成結果清單

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

ResultSetHandler 隻有一個具體實作類,即 DefaultResultSetHandler。

TypeHandler 負責将 Java 對象類型和 JDBC 類型進行互相轉換。

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

MyBatisDemo.java 檔案中的代碼片段:

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

UserMapper.xml 檔案中的代碼片段:

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

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

說明:

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 截圖:

從源碼角度分析 MyBatis 工作原理一、MyBatis 完整示例二、MyBatis 生命周期三、 MyBatis 的架構四、SqlSession 内部工作機制五、參考資料
小結:通過 "SqlSession 和 Mapper" 以及 "SqlSession 和 Executor" 這兩節,我們已經知道:SqlSession 的職能是:根據 Statement ID, 在 Configuration 中擷取到對應的 MappedStatement 對象,然後調用 Executor 來執行具體的操作。

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

(1)執行器查詢入口

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

生成動态 SQL:根據傳參,動态生成需要執行的 SQL 語句,用 BoundSql 對象表示。

管理緩存:根據傳參,建立一個緩存 Key。

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

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

命中,則将緩存中資料傳回;

不命中,則查詢資料庫:

(3)查詢資料庫

queryFromDatabase 方法的職責是調用 doQuery,向資料庫發起查詢,并将傳回的結果更新到本地緩存。

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

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

根據傳遞的參數,完成 SQL 語句的動态解析,生成 BoundSql 對象,供 StatementHandler 使用;

為查詢建立緩存,以提高性能

建立 JDBC 的 Statement 連接配接對象,傳遞給 StatementHandler 對象,傳回 List 查詢結果。

prepareStatement() 方法的實作:

對于 JDBC 的 PreparedStatement 類型的對象,建立的過程中,我們使用的是 SQL 語句字元串會包含若幹個占位符,我們其後再對占位符進行設值。

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

它會根據配置的 Statement 類型,選擇執行個體化相應的 StatementHandler,然後由其代理對象完成工作。

【源碼】RoutingStatementHandler

【源碼】RoutingStatementHandler 的 parameterize 方法源碼

【源碼】PreparedStatementHandler 的 parameterize 方法源碼

StatementHandler使用ParameterHandler對象來完成對Statement 的指派。

【源碼】StatementHandler 的 query 方法源碼

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

【源碼】DefaultParameterHandler 的 setParameters 方法

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

【源碼】DefaultResultSetHandler 的 handleResultSets 方法。handleResultSets 方法是 DefaultResultSetHandler 的最關鍵方法。其實作如下:

官方

MyBatis Github

MyBatis 官網

MyBatis Generator

Spring 內建

Spring Boot 內建

擴充插件

MyBatis-plus - CRUD 擴充插件、代碼生成器、分頁器等多功能

Mapper - CRUD 擴充插件

MyBatis-PageHelper - MyBatis 通用分頁插件

文章

《深入了解 MyBatis 原理》

《MyBatis 源碼中文注釋》

《MyBatis 中強大的 resultMap》

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