MyBatis的SQL執行流程,邏輯超清晰,總結得也太全了吧!

前言

MyBatis可能不少人都一直在用,可是MyBatis的SQL執行流程可能並非全部人都清楚了,那麼既然進來了,通讀本文你將收穫以下:java

  • 一、Mapper接口和映射文件是如何進行綁定的sql

  • 二、MyBatis中SQL語句的執行流程數據庫

  • 三、自定義MyBatis中的參數設置處理器typeHandlerapache

  • 四、自定義MyBatis中結果集處理器typeHandler編程

福利:MyBatis學習筆記與源碼分析

PS:本文基於MyBatis3.5.5版本源碼緩存

概要

在MyBatis中,利用編程式進行數據查詢,主要就是下面幾行代碼:session

SqlSession session = sqlSessionFactory.openSession();UserMapper 
userMapper = session.getMapper(UserMapper.class);List<LwUser> user
List = userMapper.listUserByUserName("孤狼1號");複製代碼

第一行是獲取一個SqlSession對象在上一篇文章分析過了,第二行就是獲取UserMapper接口,第三行一行代碼就實現了整個查詢語句的流程,接下來咱們就來仔細分析一下第二和第三步。mybatis

獲取Mapper接口(getMapper)

第二步是經過SqlSession對象是獲取一個Mapper接口,這個流程仍是相對簡單的,下面就是咱們調用session.getMapper方法以後的運行時序圖:app

圖片

  • 一、在調用getMapper以後,會去Configuration對象中獲取Mapper對象,由於在項目啓動的時候就會把Mapper接口加載並解析存儲到Configuration對象

圖片

  • 二、經過Configuration對象中的MapperRegistry對象屬性,繼續調用getMapper方法

圖片

  • 三、根據type類型,從MapperRegistry對象中的knownMappers獲取到當前類型對應的代理工廠類,而後經過代理工廠類生成對應Mapper的代理類

圖片

  • 四、最終獲取到咱們接口對應的代理類MapperProxy對象

圖片

而MapperProxy能夠看到實現了InvocationHandler,使用的就是JDK動態代理。框架

圖片

至此獲取Mapper流程結束了,那麼就有一個問題了MapperRegistry對象內的HashMap屬性knownMappers中的數據是何時存進去的呢?

Mapper接口和映射文件是什麼時候關聯的

Mapper接口及其映射文件是在加載mybatis-config配置文件的時候存儲進去的,下面就是時序圖:

圖片

  • 一、首先咱們會手動調用SqlSessionFactoryBuilder方法中的build()方法:

圖片

  • 二、而後會構造一個XMLConfigBuilder對象,並調用其parse方法:

圖片

  • 三、而後會繼續調用本身的parseConfiguration來解析配置文件,這裏面就會分別去解析全局配置文件的頂級節點,其餘的咱們先不看,咱們直接看最後解析mappers節點

圖片

  • 四、繼續調用本身的mapperElement來解析mappers文件(這個方法比較長,爲了方便截圖完整,因此把字體縮小了1號),能夠看到,這裏面分了四種方式來解析mappers節點的配置,對應了4種mapper配置方式,而其中紅框內的兩種方式是直接配置的xml映射文件,藍框內的兩種方式是解析直接配置Mapper接口的方式,從這裏也能夠說明,不論配置哪一種方式,最終MyBatis都會將xml映射文件和Mapper接口進行關聯。

圖片

  • 五、咱們先看第2種和第3中(直接配置xml映射文件的解析方式),會構建一個XMLMapperBuilder對象並調用其parse方法。

圖片

固然,這個仍是會被解析的,後面執行查詢的時候會再次經過不斷遍歷去所有解析完畢,不過有一點須要注意的是,互相引用這種是會致使解析失敗報錯的,因此在開發過程當中咱們應該避免循環依賴的產生。

  • 六、解析完映射文件以後,調用自身方法bindMapperForNamespace,開始綁定Mapper接口和映射文件:

圖片

  • 七、調用Configuration對象的addMapper

圖片

  • 八、調用Configuration對象的屬性MapperRegistry內的addMapper方法,這個方法就是正式將Mapper接口添加到knownMappers,因此上面getMapper能夠直接獲取:

圖片

到這裏咱們就完成了Mapper接口和xml映射文件的綁定

  • 九、注意上面紅框裏面的代碼,又調用了一次parse方法,這個parse方法主要是解析註解,好比下面的語句:
@Select("select * from lw_user")    List<LwUser> listAllUser();複製代碼

因此這個方法裏面會去解析@Select等註解,須要注意的是,parse方法裏面會同時再解析一次xml映射文件,由於上面咱們提到了mappers節點有4種配置方式,其中兩種配置的是Mapper接口,而配置Mapper接口會直接先調用addMapper接口,並無解析映射文件,因此進入註解解析方法parse之中會須要再嘗試解析一次XML映射文件。

圖片

圖片

解析完成以後,還會對Mapper接口中的方法進行解析,並將每一個方法的全限定類名做爲key存入存入Configuration中的mappedStatements屬性。

須要指出的是,這裏存儲的時候,同一個value會存儲2次,一個全限定名做爲key,另外一個就是隻用方法名(sql語句的id)來做爲key

圖片

因此最終mappedStatements會是下面的狀況:

圖片

事實上若是咱們經過接口的方式來編程的話,最後來getStatement的時候,都是根據全限定名來取的,因此即便有重名對咱們也沒有影響,而之因此要這麼作的緣由其實仍是爲了兼容早期版本的用法,那就是不經過接口,而是直接經過方法名的方式來進行查詢

session.selectList("com.lonelyWolf.mybatis.mapper.UserMapper.listAllUser");複製代碼

這裏若是shortName沒有重複的話,是能夠直接經過簡寫來查詢的:

session.selectList("listAllUser");複製代碼

可是經過簡寫來查詢一旦shortName重複了就會拋出如下異常:

圖片

這裏的異常其實就是StrickMap的get方法拋出來的:

圖片

sql執行流程分析

上面咱們講到了,獲取到的Mapper接口實際上被包裝成爲了代理對象,因此咱們執行查詢語句確定是執行的代理對象方法,接下來咱們就以Mapper接口的代理對象MapperProxy來分析一下查詢流程。

整個sql執行流程能夠分爲兩大步驟:

  • 1、尋找sql

  • 2、執行sql語句

尋找sql

首先仍是來看一下尋找sql語句的時序圖:

圖片

  • 一、瞭解代理模式的應該都知道,調用被代理對象的方法以後實際上執行的就是代理對象的invoke方法

圖片

  • 二、由於咱們這裏並無調用Object類中的方法,因此確定走的else。else中會繼續調用MapperProxy內部類MapperMethodInvoker中的方法cachedInvoker,這裏面會有一個判斷,判斷一下咱們是否是default方法,由於Jdk1.8中接口中能夠新增default方法,而default方法是並非一個抽象方法,因此也須要特殊處理(剛開始會從緩存裏面取,緩存相關知識咱們這裏先不講,後面會單獨寫一篇來分析一下緩存))。

圖片

  • 三、接下來,是構造一個MapperMethod對象,這個對象封裝了Mapper接口中對應的方法信息以及對應的sql語句信息:

圖片

這裏面就會把要執行的sql語句,請求參數,方法返回值所有解析封裝成MapperMethod對象,而後後面就能夠開始準備執行sql語句了

執行sql語句

仍是先來看一下執行Sql語句的時序圖:

圖片

  • 一、咱們繼續上面的流程進入execute方法:

圖片

  • 二、這裏面會根據語句類型以及返回值類型來決定如何執行,本人這裏返回的是一個集合,故而咱們進入executeForMany方法:

圖片

  • 三、這裏面首先會將前面存好的參數進行一次轉換,而後繞了這麼一圈,回到了起點SqlSession對象,繼續調用selectList方法:

圖片

  • 三、接下來又講流程委派給了Execute去執行query方法,最終又會去調用queryFromDatabase方法:

圖片

  • 四、到這裏以後,終於要進入正題了,通常帶了這種do開頭的方法就是真正作事的,Spring中不少地方也是採用的這種命名方式:

圖片

注意,前面咱們的sql語句仍是佔位符的方式,並無將參數設置進去,因此這裏在return上面一行調用prepareStatement方法建立Statement對象的時候會去設置參數,替換佔位符。參數如何設置咱們先跳過,等把流程執行完了咱們在單獨分析參數映射和結果集映射。

  • 五、繼續進入PreparedStatementHandler對象的query方法,能夠看到,這一步就是調用了jdbc操做對象PreparedStatement中的execute方法,最後一步就是轉換結果集而後返回。

圖片

到這裏,整個SQL語句執行流程分析就結束了,中途有一些參數的存儲以及轉換並無深刻進去,由於參數的轉換並非核心,只要清楚整個數據的流轉流程,咱們本身也能夠有本身的實現方式,只要存起來最後咱們能從新解析讀出來就行。

參數映射

如今咱們來看一下上面在執行查詢以前參數是如何進行設置的,咱們先進入prepareStatement方法:

圖片

咱們發現,最終是調用了StatementHandler中的parameterize進行參數設置,接下來這裏爲了節省篇幅,咱們不會一步步點進去,直接進入設置參數的方法:

圖片

上面的BaseTypeHandler是一個抽象類,setNonNullParameter並無實現,都是交給子類去實現,而每個子類就是對應了數據庫的一種類型。下圖中就是默認的一個子類StringTypeHandler,裏面沒什麼其餘邏輯,就是設置參數。

圖片

能夠看到String裏面調用了jdbc中的setString方法,而若是是int也會調用setInt方法。 看到這些子類若是你們以前閱讀過我前面講的MyBatis參數配置,應該就很明顯能夠知道,這些子類就是系統默認提供的一些typeHandler。而這些默認的typeHandler會默認被註冊並和Java對象進行綁定:

圖片

正是由於MyBatis中默認提供了經常使用數據類型的映射,因此咱們寫Sql的時候才能夠省略參數映射關係,能夠直接採用下面的方式,系統能夠根據咱們參數的類型,自動選擇合適的typeHander進行映射:

select user_id,user_name from lw_user where user_name=#{userName}複製代碼

上面這條語句實際上和下面這條是等價的:

select user_id,user_name from lw_user where user_name=#{userName,jdbcType=VARCHAR}複製代碼

或者說咱們能夠直接指定typeHandler:

select user_id,user_name from lw_user where user_name=#{userName,jdbcType=VARCHAR,typeHandler=org.apache.ibatis.type.IntegerTypeHandler}複製代碼

這裏由於咱們配置了typeHandler,因此會優先以配置的typeHandler爲主不會再去讀取默認的映射,若是類型不匹配就會直接報錯了:

圖片

看到這裏不少人應該就知道了,若是咱們本身自定義一個typeHandler,而後就能夠配置成咱們本身的自定義類。 因此接下來就讓咱們看看如何自定義一個typeHandler

自定義typeHandler

自定義typeHandler須要實現BaseTypeHandler接口,BaseTypeHandler有4個方法,包括結果集映射,爲了節省篇幅,代碼沒有寫上來:

package com.lonelyWolf.mybatis.typeHandler;import org.apache.ibatis.type.BaseTypeHandler;import org.apache.ibatis.type.JdbcType;import java.sql.CallableStatement;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;public class MyTypeHandler extends BaseTypeHandler<String> {    @Override    public void setNonNullParameter(PreparedStatement preparedStatement, int index, String param, JdbcType jdbcType) throws SQLException {        System.out.println("自定義typeHandler生效了");        preparedStatement.setString(index,param);    }複製代碼

而後咱們改寫一下上面的查詢語句:

select user_id,user_name from lw_user where user_name=#{userName,jdbcType=VARCHAR,typeHandler=com.lonelyWolf.mybatis.typeHandler.MyTypeHandler}複製代碼

而後執行,能夠看到,自定義的typeHandler生效了:

圖片

結果集映射

接下來讓咱們看看結果集的映射,回到上面執行sql流程的最後一個方法:

resultSetHandler.handleResultSets(ps)複製代碼

結果集映射裏面的邏輯相對來講仍是挺複雜的,由於要考慮到很是多的狀況,這裏咱們就不會去深究每個細節,直接進入到正式解析結果集的代碼,下面的5個代碼片斷就是一個簡單的可是完整的解析流程:

圖片

圖片

圖片

圖片

圖片

從上面的代碼片斷咱們也能夠看到,實際上解析結果集仍是很複雜的,就如咱們上一篇介紹的複雜查詢同樣,一個查詢能夠不斷嵌套其餘查詢,還有延遲加載等等一些複雜的特性 的處理,因此邏輯分支是有不少,可是無論怎麼處理,最後的核心仍是上面的一套流程,最終仍是會調用typeHandler來獲取查詢到的結果。

是的,你沒猜錯,這個就是上面咱們映射參數的typeHandler,由於typeHandler裏面不僅是一個設置參數方法,還有獲取結果集方法(上面設置參數的時候省略了)。

自定義typeHandler結果集

因此說咱們仍是用上面那個MyTypeHandler 例子來重寫一下取值方法(省略了設置參數方法):

package com.lonelyWolf.mybatis.typeHandler;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class MyTypeHandler extends BaseTypeHandler<String> {  
  /**     
* 設置參數     *
/    
@Override    public void setNonNullParameter
(PreparedStatement preparedStatement, int index, String param, JdbcType jdbcType) 
throws SQLException {      
  System.out.println("設置參數->自定義typeHandler生效了");        
preparedStatement.setString(index,param);    }  
  
/**     * 根據列名獲取結果     */    
@Override    public String getNullableResult(ResultSet resultSet, String columnName)
 throws SQLException {        System.out.println("根據columnName獲取結果->自定義typeHandler生效了");      

  return resultSet.getString(columnName);    }   
 /**     * 根據列的下標來獲取結果     
*/    
@Override    public String getNullableResult(ResultSet resultSet, int columnIndex) 
throws SQLException {        System.out.println("根據columnIndex獲取結果->自定義typeHandler生效了");  

      return resultSet.getString(columnIndex);    }    
/**     * 處理存儲過程的結果集     */    

@Override    public String getNullableResult(CallableStatement callableStatement, 
int columnIndex) throws SQLException {       
 return callableStatement.getString(columnIndex);    }}複製代碼

改寫Mapper映射文件配置:

<resultMap id="MyUserResultMap" type="lwUser">        
<result column="user_id" property="userId" jdbcType="VARCHAR" 
typeHandler="com.lonelyWolf.mybatis.typeHandler.MyTypeHandler" />        
<result column="user_name" property="userName" jdbcType="VARCHAR" />    
</resultMap><select id="listUserByUserName" parameterType="String" resultMap="MyUserResultMap">        

select user_id,user_name from lw_user where user_name=#{userName,jdbcType=VARCHAR,typeHandler=com.lonelyWolf.mybatis.
typeHandler.MyTypeHandler}    
</select>複製代碼

執行以後輸出以下:

由於咱們屬性上面只配置了一個屬性,因此只輸出了一次。

工做流程圖

上面介紹了代碼的流轉,可能繞來繞去有點暈,因此咱們來畫一個主要的對象之間流程圖來更加清晰的展現一下MyBatis主要工做流程:

圖片

從上面的工做流程圖上咱們能夠看到,SqlSession下面還有4大對象,這4大對象也很重要,後面學習攔截器的時候就是針對這4大對象進行的攔截,關於這4大對象的具體詳情,咱們下一篇文章再展開分析。

總結

本文主要分析了MyBatis的SQL執行流程。在分析流程的過程當中,咱們也舉例論證瞭如何自定義typeHandler來實現自定義的參數映射和結果集映射,不過MyBatis中提供的默認映射其實能夠知足大部分的需求,若是咱們對某些屬性須要特殊處理,那麼就能夠採用自定義的typeHandle來實現,相信若是本文若是讀懂了,如下幾點你們應該至少會有一個清晰的認識:

  • 一、Mapper接口和映射文件是如何進行綁定的

  • 二、MyBatis中SQL語句的執行流程

  • 三、自定義MyBatis中的參數設置處理器typeHandler

  • 四、自定義MyBatis中結果集處理器typeHandler

固然,其中不少細節並無提到,而看源碼咱們也並不須要追求每一行代碼都能看懂,就好比咱們一個稍微複雜一點的業務系統,即便咱們是項目開發者若是某一個模塊不是本人負責的,恐怕也很難搞清楚每一行代碼的含義。因此對於MyBatis及其餘框架的源碼中也是同樣,首先應該從大局入手,掌握總體流程和設計思想,而後若是對某些實現細節感興趣,再深刻進行了解。

相關文章
相關標籤/搜索