Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

封面:洛小汐前端

做者:潘潘java

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

2021年,仰望天空,腳踏實地。node

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

這算是春節後首篇 Mybatis 文了~ mysql

跨了個年感受寫了有半個世紀 ... android

藉着女神節 ヾ(◍°∇°◍)ノ゙git

提早祝男神女神們越靚越富越嗨森!github

上圖保存可作朋友圈封面圖 ~sql

前言

本節咱們介紹 Mybatis 的強大特性之一:動態 SQL ,從動態 SQL 的誕生背景與基礎概念,到動態 SQL 的標籤成員及基本用法,咱們徐徐道來,再結合框架源碼,剖析動態 SQL (標籤)的底層原理,最終在文末吐槽一下:在無動態 SQL 特性(標籤)以前,咱們會經常掉進哪些可惡的坑吧~數據庫

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

建議關注咱們! Mybatis 全解系列一直在更新哦apache

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

Mybaits系列全解

  • Mybatis系列全解(一):手寫一套持久層框架
  • Mybatis系列全解(二):Mybatis簡介與環境搭建
  • Mybatis系列全解(三):Mybatis簡單CRUD使用介紹
  • Mybatis系列全解(四):全網最全!Mybatis配置文件XML全貌詳解
  • Mybatis系列全解(五):全網最全!詳解Mybatis的Mapper映射文件
  • Mybatis系列全解(六):Mybatis最硬核的API你知道幾個?
  • Mybatis系列全解(七):Dao層的兩種實現之傳統與代理
  • Mybatis系列全解(八):Mybatis的動態SQL
  • Mybatis系列全解(九):Mybatis的複雜映射
  • Mybatis系列全解(十):Mybatis註解開發
  • Mybatis系列全解(十一):Mybatis緩存全解
  • Mybatis系列全解(十二):Mybatis插件開發
  • Mybatis系列全解(十三):Mybatis代碼生成器
  • Mybatis系列全解(十四):Spring集成Mybatis
  • Mybatis系列全解(十五):SpringBoot集成Mybatis
  • Mybatis系列全解(十六):Mybatis源碼剖析

本文目錄

一、什麼是動態SQL

二、動態SQL的誕生記

三、動態SQL標籤的9大標籤

四、動態SQL的底層原理

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

一、什麼是動態SQL ?

關於動態 SQL ,容許咱們理解爲 「 動態的 SQL 」,其中 「 動態的 」 是形容詞,「 SQL 」 是名詞,那顯然咱們須要先理解名詞,畢竟形容詞僅僅表明它的某種形態或者某種狀態。

SQL 的全稱是:

Structured Query Language,結構化查詢語言。

SQL 自己好說,咱們小學時候都學習過了,無非就是 CRUD 嘛,並且咱們還知道它是一種 語言,語言是一種存在於對象之間用於交流表達的 能力,例如跟中國人交流用漢語、跟英國人交流用英語、跟火星人交流用火星語、跟小貓交流用喵喵語、跟計算機交流咱們用機器語言、跟數據庫管理系統(DBMS)交流咱們用 SQL。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

想必你們立馬就能明白,想要與某個對象交流,必須擁有與此對象交流的語言能力才行!因此不管是技術人員、仍是應用程序系統、或是某個高級語言環境,想要訪問/操做數據庫,都必須具有 SQL 這項能力;所以你能看到像 Java ,像 Python ,像 Go 等等這些高級語言環境中,都會嵌入(支持) SQL 能力,達到與數據庫交互的目的。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

很顯然,可以學習 Mybatis 這麼一門高精尖(ru-men)持久層框架的編程人羣,對於 SQL 的編寫能力確定已經掌握得 ss 的,平時各類 SQL 編寫那都是信手拈來的事, 只不過對於 動態SQL 究竟是個什麼東西,彷佛還有一些朋友似懂非懂!可是不要緊,咱們百度一下。

動態 SQL:通常指根據用戶輸入或外部條件 動態組合 的 SQL 語句塊。

很容易理解,隨外部條件動態組合的 SQL 語句塊!咱們先針對動態 SQL 這個詞來剖析,世間萬物,有動態那就相對應的有靜態,那麼他們的邊界在哪裏呢?又該怎麼區分呢?

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

其實,上面咱們已經介紹過,在例如 Java 高級語言中,都會嵌入(支持)SQL 能力,通常咱們能夠直接在代碼或配置文件中編寫 SQL 語句,若是一個 SQL 語句在 「編譯階段」 就已經能肯定 主體結構,那咱們稱之爲靜態 SQL,若是一個 SQL 語句在編譯階段沒法肯定主體結構,須要等到程序真正 「運行時」 才能最終肯定,那麼咱們稱之爲動態 SQL,舉個例子:

<!-- 一、定義SQL -->
<mapper namespace="dao">
    <select id="selectAll" resultType="user">
        select * from t_user
    </select>
</mapper>
// 二、執行SQL
sqlSession.select("dao.selectAll");

很明顯,以上這個 SQL ,在編譯階段咱們都已經知道它的主體結構,即查詢 t_user 表的全部記錄,而無需等到程序運行時才肯定這個主體結構,所以以上屬於 靜態 SQL。那咱們再看看下面這個語句:

<!-- 一、定義SQL -->
<mapper namespace="dao">
    <select id="selectAll" parameterType="user">
        select * from t_user 
        <if test="id != null">
            where id = #{id}
        </if>
    </select>
</mapper>
// 二、執行SQL
User user1 = new User();
user1.setId(1);
sqlSession.select("dao.selectAll",user1);  // 有 id

User user2 = new User(); 
sqlSession.select("dao.selectAll",user2);  // 無 id

認真觀察,以上這個 SQL 語句,額外添加了一塊 if 標籤 做爲條件判斷,因此應用程序在編譯階段是沒法肯定 SQL 語句最終主體結構的,只有在運行時根據應用程序是否傳入 id 這個條件,來動態的拼接最終執行的 SQL 語句,所以屬於動態 SQL 。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

另外,還有一種常見的狀況,你們看看下面這個 SQL 語句算是動態 SQL 語句嗎?

<!-- 一、定義SQL -->
<mapper namespace="dao">
    <select id="selectAll" parameterType="user">
        select * from t_user where id = #{id} 
    </select>
</mapper>
// 二、執行SQL
User user1 = new User();
user1.setId(1);
sqlSession.select("dao.selectAll",user1);  // 有 id

根據動態 SQL 的定義,你們是否能判斷以上的語句塊是否屬於動態 SQL?

答案:不屬於動態 SQL !

緣由很簡單,這個 SQL 在編譯階段就已經明確主體結構了,雖然外部動態的傳入一個 id ,多是1,多是2,多是100,可是由於它的主體結構已經肯定,這個語句就是查詢一個指定 id 的用戶記錄,它最終執行的 SQL 語句不會有任何動態的變化,因此頂多算是一個支持動態傳參的靜態 SQL 。

至此,咱們對於動態 SQL 和靜態 SQL 的區別已經有了一個基礎認知,可是有些好奇的朋友又會思考另外一個問題:動態 SQL 是 Mybatis 獨有的嗎?

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

二、動態SQL的誕生記

咱們都知道,SQL 是一種偉大的數據庫語言 標準,在數據庫管理系統紛爭的時代,它的出現統一規範了數據庫操做語言,而此時,市面上的數據庫管理軟件百花齊放,我最先使用的 SQL Server 數據庫,當時用的數據庫管理工具是 SQL Server Management Studio,後來接觸 Oracle 數據庫,用了 PL/SQL Developer,再後來直至今日就幾乎都在用 MySQL 數據庫(這個跟各類雲廠商崛起有關),因此基本使用 Navicat 做爲數據庫管理工具,固然現在市面上還有許多許多,數據庫管理工具嘛,只要能便捷高效的管理咱們的數據庫,那就是好工具,duck 沒必要糾結選擇哪一款!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

那這麼多好工具,都提供什麼功能呢?相信咱們平時接觸最多的就是接收執行 SQL 語句的輸入界面(也稱爲查詢編輯器),這個輸入界面幾乎支持全部 SQL 語法,例如咱們編寫一條語句查詢 id 等於15 的用戶數據記錄:

select * from user where id = 15 ;

咱們來看一下這個查詢結果:

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

很顯然,在這個輸入界面內輸入的任何 SQL 語句,對於數據庫管理工具來講,都是 動態 SQL!由於工具自己並不可能提早知道用戶會輸入什麼 SQL 語句,只有當用戶執行以後,工具才接收到用戶實際輸入的 SQL 語句,才能最終肯定 SQL 語句的主體結構,固然!即便咱們不經過可視化的數據庫管理工具,也能夠用數據庫自己自帶支持的命令行工具來執行 SQL 語句。但不管用戶使用哪類工具,輸入的語句都會被工具認爲是 動態 SQL

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

這麼一說,動態 SQL 原來不是 Mybatis 獨有的特性!其實除了以上介紹的數據庫管理工具之外,在純 JDBC 時代,咱們就常常經過字符串來動態的拼接 SQL 語句,這也是在高級語言環境(例如 Java 語言編程環境)中早期經常使用的動態 SQL 構建方式!

// 外部條件id
Integer id = Integer.valueOf(15);

// 動態拼接SQL
StringBuilder sql = new StringBuilder();
sql.append(" select  *   ");
sql.append("   from user ");

// 根據外部條件id動態拼接SQL
if ( null != id ){
    sql.append(" where id = " + id);
}

// 執行語句
connection.prepareStatement(sql);

只不過,這種構建動態 SQL 的方式,存在很大的安全問題和異常風險(咱們第5點會詳細介紹),因此不建議使用,後來 Mybatis 入世以後,在對待動態 SQL 這件事上,就格外上心,它默默發誓,必定要爲使用 Mybatis 框架的用戶提供一套棒棒的方案(標籤)來靈活構建動態 SQL!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

因而乎,Mybatis 藉助 OGNL 的表達式的偉大設計,可算在動態 SQL 構建方面提供了各種功能強大的輔助標籤,咱們簡單列舉一下有:if、choose、when、otherwise、trim、where、set、foreach、bind等,我隨手翻了翻我電腦裏頭曾經保存的學習筆記,咱們一塊兒在第3節中溫故知新,詳細的講一講吧~

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

另外,須要糾正一點,就是咱們平日裏在 Mybatis 框架中常說的動態 SQL ,其實特指的也就是 Mybatis 框架中的這一套動態 SQL 標籤,或者說是這一 特性,而並非在說動態 SQL 自己。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

三、動態SQL標籤的9大標籤

很好,可算進入咱們動態 SQL 標籤的主題,根據前面的鋪墊,其實咱們都能發現,不少時候靜態 SQL 語句並不能知足咱們複雜的業務場景需求,因此咱們須要有適當靈活的一套方式或者能力,來便捷高效的構建動態 SQL 語句,去匹配咱們動態變化的業務需求。舉個栗子,在下面此類多條件的場景需求之下,動態 SQL 語句就顯得尤其重要(先登場 if 標籤)。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

固然,不少朋友會說這類需求,不能用 SQL 來查,得用搜索引擎,確實如此。可是呢,在咱們的實際業務需求當中,仍是存在不少沒有引入搜索引擎系統,或者有些根本無需引入搜索引擎的應用程序或功能,它們也會涉及到多選項多條件或者多結果的業務需求,那此時也就確實須要使用動態 SQL 標籤來靈活構建執行語句。

那麼, Mybatis 目前都提供了哪些棒棒的動態 SQL 標籤呢 ?咱們先引出一個類叫作 XMLScriptBuilder ,你們先簡單理解它是負責解析咱們的動態 SQL 標籤的這麼一個構建器,在第4點底層原理中咱們再詳細介紹。

// XML腳本標籤構建器
public class XMLScriptBuilder{

    // 標籤節點處理器池
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

    // 構造器
    public XMLScriptBuilder() { 
        initNodeHandlerMap();
        //... 其它初始化不贅述也不重要
    }

    // 初始化
    private void initNodeHandlerMap() {
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("where", new WhereHandler());
        nodeHandlerMap.put("set", new SetHandler());
        nodeHandlerMap.put("foreach", new ForEachHandler());
        nodeHandlerMap.put("if", new IfHandler());
        nodeHandlerMap.put("choose", new ChooseHandler());
        nodeHandlerMap.put("when", new IfHandler());
        nodeHandlerMap.put("otherwise", new OtherwiseHandler());
        nodeHandlerMap.put("bind", new BindHandler());
    }
}

其實源碼中很清晰得體現,一共有 9 大動態 SQL 標籤!Mybatis 在初始化解析配置文件的時候,會實例化這麼一個標籤節點的構造器,那麼它自己就會提早把全部 Mybatis 支持的動態 SQL 標籤對象對應的處理器給進行一個實例化,而後放到一個 Map 池子裏頭,而這些處理器,都是該類 XMLScriptBuilder 的一個匿名內部類,而匿名內部類的功能也很簡單,就是解析處理對應類型的標籤節點,在後續應用程序使用動態標籤的時候,Mybatis 隨時到 Map 池子中匹配對應的標籤節點處理器,而後進解析便可。下面咱們分別對這 9 大動態 SQL 標籤進行介紹,排(gen)名(ju)不(wo)分(de)先(xi)後(hao):


Top一、if 標籤

經常使用度:★★★★★

實用性:★★★★☆

if 標籤,絕對算得上是一個偉大的標籤,任何不支持流程控制(或語句控制)的應用程序,都是耍流氓,幾乎都不具有現實意義,實際的應用場景和流程必然存在條件的控制與流轉,而 if 標籤在 單條件分支判斷 應用場景中就起到了捨我其誰的做用,語法很簡單,若是知足,則執行,不知足,則忽略/跳過。

  • if 標籤 : 內嵌於 select / delete / update / insert 標籤,若是知足 test 屬性的條件,則執行代碼塊
  • test 屬性 :做爲 if 標籤的屬性,用於條件判斷,使用 OGNL 表達式。

舉個例子:

<select id="findUser">
    select * from User where 1=1
    <if test=" age != null ">
        and age > #{age}
    </if>
    <if test=" name != null ">
        and name like concat(#{name},'%')
    </if>
</select>

很明顯,if 標籤元素經常使用於包含 where 子句的條件拼接,它至關於 Java 中的 if 語句,和 test 屬性搭配使用,經過判斷參數值來決定是否使用某個查詢條件,也可用於 Update 語句中判斷是否更新某個字段,或用於 Insert 語句中判斷是否插入某個字段的值。

每個 if 標籤在進行單條件判斷時,須要把判斷條件設置在 test 屬性中,這是一個常見的應用場景,咱們經常使用的用戶查詢系統功能中,在前端通常提供不少可選的查詢項,支持性別篩選、年齡區間篩查、姓名模糊匹配等,那麼咱們程序中接收用戶輸入以後,Mybatis 的動態 SQL 節省咱們不少工做,容許咱們在代碼層面不進行參數邏輯處理和 SQL 拼接,而是把參數傳入到 SQL 中進行條件判斷動態處理,咱們只須要把精力集中在 XML 的維護上,既靈活也方便維護,可讀性還強。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

有些心細的朋友可能就發現一個問題,爲何 where 語句會添加一個 1=1 呢?其實咱們是爲了方便拼接後面符合條件的 if 標籤語句塊,不然沒有 1=1 的話咱們拼接的 SQL 就會變成 select * from user where and age > 0 , 顯然這不是咱們指望的結果,固然也不符合 SQL 的語法,數據庫也不可能執行成功,因此咱們投機取巧添加了 1=1 這個語句,可是始終以爲多餘且不必,Mybatis 也考慮到了,因此等會咱們講 where 標籤,它是如何完美解決這個問題的。

注意:if 標籤做爲單條件分支判斷,只能控制與非此即彼的流程,例如以上的例子,若是年齡 age 和姓名 name 都不存在,那麼系統會把全部結果都查詢出來,但有些時候,咱們但願系統更加靈活,能有更多的流程分支,例如像咱們 Java 當中的 if else 或 switch case default,不只僅只有一個條件分支,因此接下來咱們介紹 choose 標籤,它就能知足多分支判斷的應用場景。


Top二、choose 標籤、when 標籤、otherwise 標籤

經常使用度:★★★★☆

實用性:★★★★☆

有些時候,咱們並不但願條件控制是非此即彼的,而是但願能提供多個條件並從中選擇一個,因此貼心的 Mybatis 提供了 choose 標籤元素,相似咱們 Java 當中的 if else 或 switch case default,choose 標籤必須搭配 when 標籤和 otherwise 標籤使用,驗證條件依然是使用 test 屬性進行驗證。

  • choose 標籤:頂層的多分支標籤,單獨使用無心義
  • when 標籤:內嵌於 choose 標籤之中,當知足某個 when 條件時,執行對應的代碼塊,並終止跳出 choose 標籤,choose 中必須至少存在一個 when 標籤,不然無心義
  • otherwise 標籤:內嵌於 choose 標籤之中,當不知足全部 when 條件時,則執行 otherwise 代碼塊,choose 中 至多 存在一個 otherwise 標籤,能夠不存在該標籤
  • test 屬性 :做爲 when 與 otherwise 標籤的屬性,做爲條件判斷,使用 OGNL 表達式

依據下面的例子,當應用程序輸入年齡 age 或者姓名 name 時,會執行對應的 when 標籤內的代碼塊,若是 when 標籤的年齡 age 和姓名 name 都不知足,則會拼接 otherwise 標籤內的代碼塊。

<select id="findUser">
    select * from User where 1=1 
    <choose>
        <when test=" age != null ">
            and age > #{age}
        </when>
        <when test=" name != null ">
            and name like concat(#{name},'%')
        </when>
        <otherwise>
            and sex = '男'
        </otherwise>
    </choose>
</select>

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

很明顯,choose 標籤做爲多分支條件判斷,提供了更多靈活的流程控制,同時 otherwise 的出現也爲程序流程控制兜底,有時可以避免部分系統風險、過濾部分條件、避免當程序沒有匹配到條件時,把整個數據庫資源所有查詢或更新。

至於爲什麼 choose 標籤這麼棒棒,而經常使用度仍是比 if 標籤少了一顆星呢?緣由也簡單,由於 choose 標籤的不少使用場景能夠直接用 if 標籤代替。另外據我統計,if 標籤在實際業務應用當中,也要多於 choose 標籤,你們也能夠具體覈查本身的應用程序中動態 SQL 標籤的佔比狀況,統計分析一下。


Top三、foreach 標籤

經常使用度:★★★☆☆

實用性:★★★★☆

有些場景,可能須要查詢 id 在 1 ~ 100 的用戶記錄

有些場景,可能須要批量插入 100 條用戶記錄

有些場景,可能須要更新 500 個用戶的姓名

有些場景,可能須要你刪除 10 條用戶記錄

請問你們

不少增刪改查場景,操做對象都是集合/列表

若是是你來設計支持 Mybatis 的這一類集合/列表遍歷場景,你會提供什麼能力的標籤來輔助構建你的 SQL 語句從而去知足此類業務場景呢?

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

額(⊙o⊙)…

那若是必定要用 Mybatis 框架呢?

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

沒錯,確實 Mybatis 提供了 foreach 標籤來處理這幾類須要遍歷集合的場景,foreach 標籤做爲一個循環語句,他可以很好的支持數組、Map、或實現了 Iterable 接口(List、Set)等,尤爲是在構建 in 條件語句的時候,咱們常規的用法都是 id in (1,2,3,4,5 ... 100) ,理論上咱們能夠在程序代碼中拼接字符串而後經過 ${ ids } 方式來傳值獲取,可是這種方式不能防止 SQL 注入風險,同時也特別容易拼接錯誤,因此咱們此時就須要使用 #{} + foreach 標籤來配合使用,以知足咱們實際的業務需求。譬如咱們傳入一個 List 列表查詢 id 在 1 ~ 100 的用戶記錄:

<select id="findAll">
    select  * from user where ids in 
    <foreach collection="list"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

最終拼接完整的語句就變成:

select  * from user where ids in (1,2,3,...,100);

固然你也能夠這樣編寫:

<select id="findAll">
    select  * from user where 
    <foreach collection="list"
        item="item" index="index" 
        open=" " separator=" or " close=" ">
            id = #{item}
    </foreach>
</select>

最終拼接完整的語句就變成:

select  * from user where id =1 or id =2 or id =3  ... or id = 100;

在數據量大的狀況下這個性能會比較尷尬,這裏僅僅作一個用法的舉例。因此通過上面的舉慄,相信你們也基本能猜出 foreach 標籤元素的基本用法:

  • foreach 標籤:頂層的遍歷標籤,單獨使用無心義
  • collection 屬性:必填,Map 或者數組或者列表的屬性名(不一樣類型的值獲取下面會講解)
  • item 屬性:變量名,值爲遍歷的每個值(能夠是對象或基礎類型),若是是對象那麼依舊是 OGNL 表達式取值便可,例如 #{item.id} 、#{ user.name } 等
  • index 屬性:索引的屬性名,在遍歷列表或數組時爲當前索引值,當迭代的對象時 Map 類型時,該值爲 Map 的鍵值(key)
  • open 屬性:循環內容開頭拼接的字符串,能夠是空字符串
  • close 屬性:循環內容結尾拼接的字符串,能夠是空字符串
  • separator 屬性:每次循環的分隔符

第一,當傳入的參數爲 List 對象時,系統會默認添加一個 key 爲 'list' 的值,把列表內容放到這個 key 爲 list 的集合當中,在 foreach 標籤中能夠直接經過 collection="list" 獲取到 List 對象,不管你傳入時使用 kkk 或者 aaa ,都無所謂,系統都會默認添加一個 key 爲 list 的值,而且 item 指定遍歷的對象值,index 指定遍歷索引值。

// java 代碼
List kkk = new ArrayList();
kkk.add(1);
kkk.add(2);
...
kkk.add(100);
sqlSession.selectList("findAll",kkk);
<!-- xml 配置 -->
<select id="findAll">
    select  * from user where ids in 
    <foreach collection="list"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

第二,當傳入的參數爲數組時,系統會默認添加一個 key 爲 'array' 的值,把列表內容放到這個 key 爲 array 的集合當中,在 foreach 標籤中能夠直接經過 collection="array" 獲取到數組對象,不管你傳入時使用 ids 或者 aaa ,都無所謂,系統都會默認添加一個 key 爲 array 的值,而且 item 指定遍歷的對象值,index 指定遍歷索引值。

// java 代碼
String [] ids = new String[3];
ids[0] = "1";
ids[1] = "2";
ids[2] = "3";
sqlSession.selectList("findAll",ids);
<!-- xml 配置 -->
<select id="findAll">
    select  * from user where ids in 
    <foreach collection="array"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

第三,當傳入的參數爲 Map 對象時,系統並 不會 默認添加一個 key 值,須要手工傳入,例如傳入 key 值爲 map2 的集合對象,在 foreach 標籤中能夠直接經過 collection="map2" 獲取到 Map 對象,而且 item 表明每次迭代的的 value 值,index 表明每次迭代的 key 值。其中 item 和 index 的值名詞能夠隨意定義,例如 item = "value111",index ="key111"。

// java 代碼
Map map2 = new HashMap<>();
map2.put("k1",1);
map2.put("k2",2);
map2.put("k3",3);

Map map1 = new HashMap<>();
map1.put("map2",map2);
sqlSession.selectList("findAll",map1);

挺鬧心,map1 套着 map2,才能在 foreach 的 collection 屬性中獲取到。

<!-- xml 配置 -->
<select id="findAll">
    select  * from user where
    <foreach collection="map2"
        item="value111" index="key111" 
        open=" " separator=" or " close=" ">
        id = #{value111}
    </foreach>
</select>

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

可能你會以爲 Map 受到不公平對待,爲什麼 map 不能像 List 或者 Array 同樣,在框架默認設置一個 'map' 的 key 值呢?但其實不是不公平,而是咱們在 Mybatis 框架中,全部傳入的任何參數都會供上下文使用,因而參數會被統一放到一個內置參數池子裏面,這個內置參數池子的數據結構是一個 map 集合,而這個 map 集合能夠經過使用 「_parameter」 來獲取,全部 key 都會存儲在 _parameter 集合中,所以:

  • 當你傳入的參數是一個 list 類型時,那麼這個參數池子須要有一個 key 值,以供上下文獲取這個 list 類型的對象,因此默認設置了一個 'list' 字符串做爲 key 值,獲取時經過使用 _parameter.list 來獲取,通常使用 list 便可。
  • 一樣的,當你傳入的參數是一個 array 數組時,那麼這個參數池子也會默認設置了一個 'array' 字符串做爲 key 值,以供上下文獲取這個 array 數組的對象值,獲取時經過使用 _parameter.array 來獲取,通常使用 array 便可。
  • 可是!當你傳入的參數是一個 map 集合類型時,那麼這個參數池就不必爲你添加默認 key 值了,由於 map 集合類型自己就會有不少 key 值,例如你想獲取 map 參數的某個 key 值,你能夠直接使用 _parameter.name 或者 _parameter.age 便可,就不必還用 _parameter.map.name 或者 _parameter.map.age ,因此這就是 map 參數類型無需再構建一個 'map' 字符串做爲 key 的緣由,對象類型也是如此,例如你傳入一個 User 對象。

所以,若是是 Map 集合,你能夠這麼使用:

// java 代碼
Map map2 = new HashMap<>();
map2.put("k1",1);
map2.put("k2",2);
map2.put("k3",3); 
sqlSession.selectList("findAll",map2);

直接使用 collection="_parameter",你會發現神奇的 key 和 value 都能經過 _parameter 遍歷在 index 與 item 之中。

<!-- xml 配置 -->
<select id="findAll">
    select  * from user where
    <foreach collection="_parameter"
         item="value111" index="key111"
         open=" " separator=" or " close=" ">
        id = #{value111}
    </foreach>
</select>

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

延伸:當傳入參數爲多個對象時,例如傳入 User 和 Room 等,那麼經過內置參數獲取對象可使用 _parameter.get(0).username,或者 _parameter.get(1).roomname 。假如你傳入的參數是一個簡單數據類型,例如傳入 int =1 或者 String = '你好',那麼均可以直接使用 _parameter 代替獲取值便可,這就是不少人會在動態 SQL 中直接使用 # { _parameter } 來獲取簡單數據類型的值。

那到這裏,咱們基本把 foreach 基本用法介紹完成,不過以上只是針對查詢的使用場景,對於刪除、更新、插入的用法,也是大同小異,咱們簡單說一下,若是你但願批量插入 100 條用戶記錄:

<insert id="insertUser" parameterType="java.util.List">
    insert into user(id,username) values
    <foreach collection="list" 
         item="user" index="index"
         separator="," close=";" >
        (#{user.id},#{user.username})
    </foreach>
</insert>

若是你但願更新 500 個用戶的姓名:

<update id="updateUser" parameterType="java.util.List">
    update user 
       set username = '潘潘' 
     where id in 
    <foreach collection="list"
        item="user" index="index" 
        separator="," open="(" close=")" >
        #{user.id}    
    </foreach>
</update>

若是你但願你刪除 10 條用戶記錄:

<delete id="deleteUser" parameterType="java.util.List">
    delete from user  
          where id in 
    <foreach collection="list"
         item="user" index="index" 
         separator="," open="(" close=")" >
        #{user.id}    
    </foreach>
</delete>

更多玩法,期待你本身去挖掘!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

注意:使用 foreach 標籤時,須要對傳入的 collection 參數(List/Map/Set等)進行爲空性判斷,不然動態 SQL 會出現語法異常,例如你的查詢語句多是 select * from user where ids in () ,致使以上語法異常就是傳入參數爲空,解決方案能夠用 if 標籤或 choose 標籤進行爲空性判斷處理,或者直接在 Java 代碼中進行邏輯處理便可,例如判斷爲空則不執行 SQL 。


Top四、where 標籤、set 標籤

經常使用度:★★☆☆☆

實用性:★★★★☆

咱們把 where 標籤和 set 標籤放置一塊兒講解,一是這兩個標籤在實際應用開發中經常使用度確實不分伯仲,二是這兩個標籤出自一家,都繼承了 trim 標籤,放置一塊兒方便咱們比對追根。(其中底層原理會在第4部分詳細講解)

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

以前咱們介紹 if 標籤的時候,相信你們都已經看到,咱們在 where 子句後面拼接了 1=1 的條件語句塊,目的是爲了保證後續條件可以正確拼接,之前在程序代碼中使用字符串拼接 SQL 條件語句經常如此使用,可是確實此種方式不夠體面,也顯得咱們不高級。

<select id="findUser">
    select * from User where 1=1
    <if test=" age != null ">
        and age > #{age}
    </if>
    <if test=" name != null ">
        and name like concat(#{name},'%')
    </if>
</select>

以上是咱們使用 1=1 的寫法,那 where 標籤誕生以後,是怎麼巧妙處理後續的條件語句的呢?

<select id="findUser">
    select * from User 
    <where>
        <if test=" age != null ">
            and age > #{age}
        </if>
        <if test=" name != null ">
            and name like concat(#{name},'%')
        </if>
    </where>
</select>

咱們只需把 where 關鍵詞以及 1=1 改成 < where > 標籤便可,另外還有一個特殊的處理能力,就是 where 標籤可以智能的去除(忽略)首個知足條件語句的前綴,例如以上條件若是 age 和 name 都知足,那麼 age 前綴 and 會被智能去除掉,不管你是使用 and 運算符或是 or 運算符,Mybatis 框架都會幫你智能處理。

用法特別簡單,咱們用官術總結一下

  • where 標籤:頂層的遍歷標籤,須要配合 if 標籤使用,單獨使用無心義,而且只會在子元素(如 if 標籤)返回任何內容的狀況下才插入 WHERE 子句。另外,若子句的開頭爲 「AND」 或 「OR」,where 標籤也會將它替換去除。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

瞭解了基本用法以後,咱們再看看剛剛咱們的例子中:

<select id="findUser">
    select * from User 
    <where>
        <if test=" age != null ">
            and age > #{age}
        </if>
        <if test=" name != null ">
            and name like concat(#{name},'%')
        </if>
    </where>
</select>

若是 age 傳入有效值 10 ,知足 age != null 的條件以後,那麼就會返回 where 標籤並去除首個子句運算符 and,最終的 SQL 語句會變成:

select * from User where age > 10; 
-- and 巧妙的不見了

值得注意的是,where 標籤 只會 智能的去除(忽略)首個知足條件語句的前綴,因此就建議咱們在使用 where 標籤的時候,每一個語句都最好寫上 and 前綴或者 or 前綴,不然像如下寫法就頗有可能出大事:

<select id="findUser">
    select * from User 
    <where>
        <if test=" age != null ">
             age > #{age} 
             <!-- age 前綴沒有運算符-->
        </if>
        <if test=" name != null ">
             name like concat(#{name},'%')
             <!-- name 前綴也沒有運算符-->
        </if>
    </where>
</select>

當 age 傳入 10,name 傳入 ‘潘潘’ 時,最終的 SQL 語句是:

select * from User 
where 
age > 10 
name like concat('潘%')
-- 全部條件都沒有and或or運算符
-- 這讓age和name顯得很尷尬~

因爲 name 前綴沒有寫 and 或 or 鏈接符,而 where 標籤又不會智能的去除(忽略)非首個 知足條件語句的前綴,因此當 age 條件語句與 name 條件語句同時成立時,就會致使語法錯誤,這個須要謹慎使用,格外注意!原則上每一個條件子句都建議在句首添加運算符 and 或 or ,首個條件語句可添加可不加。

另外還有一個值得注意的點,咱們使用 XML 方式配置 SQL 時,若是在 where 標籤以後添加了註釋,那麼當有子元素知足條件時,除了 < !-- --> 註釋會被 where 忽略解析之外,其它註釋例如 // 或 /**/ 或 -- 等都會被 where 當成首個子句元素處理,致使後續真正的首個 AND 子句元素或 OR 子句元素沒能被成功替換掉前綴,從而引發語法錯誤!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

基於 where 標籤元素的講解,有助於咱們快速理解 set 標籤元素,畢竟它倆是如此相像。咱們回憶一下以往咱們的更新 SQL 語句:

<update id="updateUser">
    update user 
       set age = #{age},
           username = #{username},
           password = #{password} 
     where id =#{id}
</update>

以上語句是咱們平常用於更新指定 id 對象的 age 字段、 username 字段以及 password 字段,可是不少時候,咱們可能只但願更新對象的某些字段,而不是每次都更新對象的全部字段,這就使得咱們在語句結構的構建上顯得慘白無力。因而有了 set 標籤元素。

用法與 where 標籤元素類似

  • set 標籤:頂層的遍歷標籤,須要配合 if 標籤使用,單獨使用無心義,而且只會在子元素(如 if 標籤)返回任何內容的狀況下才插入 set 子句。另外,若子句的 開頭或結尾 都存在逗號 「,」 則 set 標籤都會將它替換去除。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

根據此用法咱們能夠把以上的例子改成:

<update id="updateUser">
    update user 
        <set>
           <if test="age !=null">
               age = #{age},
           </if>
           <if test="username !=null">
               username = #{username},
           </if> 
           <if test="password !=null">
               password = #{password},
           </if>
        </set>    
     where id =#{id}
</update>

很簡單易懂,set 標籤會智能拼接更新字段,以上例子若是傳入 age =10 和 username = '潘潘' ,則有兩個字段知足更新條件,因而 set 標籤會智能拼接 " age = 10 ," 和 "username = '潘潘' ," 。其中因爲後一個 username 屬於最後一個子句,因此末尾逗號會被智能去除,最終的 SQL 語句是:

update user set age = 10,username =  '潘潘'

另外須要注意,set 標籤下須要保證至少有一個條件知足,不然依然會產生語法錯誤,例如在無子句條件知足的場景下,最終的 SQL 語句會是這樣:

update user ;  ( oh~ no!)

既不會添加 set 標籤,也沒有子句更新字段,因而語法出現了錯誤,因此相似這類狀況,通常須要在應用程序中進行邏輯處理,判斷是否存在至少一個參數,不然不執行更新 SQL 。因此原則上要求 set 標籤下至少存在一個條件知足,同時每一個條件子句都建議在句末添加逗號 ,最後一個條件語句可加可不加。或者 每一個條件子句都在句首添加逗號 ,第一個條件語句可加可不加,例如:

<update id="updateUser">
    update user 
        <set>
           <if test="age !=null">
               ,age = #{age}
           </if>
           <if test="username !=null">
               ,username = #{username}
           </if> 
           <if test="password !=null">
               ,password = #{password}
           </if>
        </set>    
     where id =#{id}
</update>

與 where 標籤相同,咱們使用 XML 方式配置 SQL 時,若是在 set 標籤子句末尾添加了註釋,那麼當有子元素知足條件時,除了 < !-- --> 註釋會被 set 忽略解析之外,其它註釋例如 // 或 /**/ 或 -- 等都會被 set 標籤當成末尾子句元素處理,致使後續真正的末尾子句元素的逗號沒能被成功替換掉後綴,從而引發語法錯誤!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

到此,咱們的 where 標籤元素與 set 標籤就基本介紹完成,它倆確實極爲類似,區別僅在於:

  • where 標籤插入前綴 where
  • set 標籤插入前綴 set
  • where 標籤僅智能替換前綴 AND 或 OR
  • set 標籤能夠只能替換前綴逗號,或後綴逗號,

而這二者的先後綴去除策略,都源自於 trim 標籤的設計,咱們一塊兒看看到底 trim 標籤是有多靈活!


Top五、trim 標籤

經常使用度:★☆☆☆☆

實用性:★☆☆☆☆

上面咱們介紹了 where 標籤與 set 標籤,它倆的共同點無非就是前置關鍵詞 where 或 set 的插入,以及先後綴符號(例如 AND | OR | ,)的智能去除。基於 where 標籤和 set 標籤自己都繼承了 trim 標籤,因此 trim 標籤的大體實現咱們也能猜出個一二三。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

其實 where 標籤和 set 標籤都只是 trim 標籤的某種實現方案,trim 標籤底層是經過 TrimSqlNode 類來實現的,它有幾個關鍵屬性:

  • prefix :前綴,當 trim 元素內存在內容時,會給內容插入指定前綴
  • suffix :後綴,當 trim 元素內存在內容時,會給內容插入指定後綴
  • prefixesToOverride :前綴去除,支持多個,當 trim 元素內存在內容時,會把內容中匹配的前綴字符串去除。
  • suffixesToOverride :後綴去除,支持多個,當 trim 元素內存在內容時,會把內容中匹配的後綴字符串去除。

因此 where 標籤若是經過 trim 標籤實現的話能夠這麼編寫:(

<!--
  注意在使用 trim 標籤實現 where 標籤能力時
  必須在 AND 和 OR 以後添加空格
  避免匹配到 android、order 等單詞 
-->
<trim prefix="WHERE" prefixOverrides="AND | OR" >
    ...
</trim>

而 set 標籤若是經過 trim 標籤實現的話能夠這麼編寫:

<trim prefix="SET" prefixOverrides="," >
    ...
</trim>

或者

<trim prefix="SET" suffixesToOverride="," >
    ...
</trim>

因此可見 trim 是足夠靈活的,不過因爲 where 標籤和 set 標籤這兩種 trim 標籤變種方案已經足以知足咱們實際開發需求,因此直接使用 trim 標籤的場景實際上不太不少(實際上是我本身使用的很少,基本沒用過)。

注意,set 標籤之因此可以支持去除前綴逗號或者後綴逗號,是因爲其在構造 trim 標籤的時候進行了前綴後綴的去除設置,而 where 標籤在構造 trim 標籤的時候就僅僅設置了前綴去除。

set 標籤元素之構造時:

// Set 標籤
public class SetSqlNode extends TrimSqlNode {

  private static final List<String> COMMA = Collections.singletonList(",");

  // 明顯使用了前綴後綴去除,注意先後綴參數都傳入了 COMMA 
  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }

}

where 標籤元素之構造時:

// Where 標籤
public class WhereSqlNode extends TrimSqlNode {

  // 其實包含了不少種場景
  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  // 明顯只使用了前綴去除,注意前綴傳入 prefixList,後綴傳入 null 
  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "WHERE", prefixList, null, null);
  }

}

Top六、bind 標籤

經常使用度:☆☆☆☆☆

實用性:★☆☆☆☆

簡單來講,這個標籤就是能夠建立一個變量,並綁定到上下文,即供上下文使用,就是這樣,我把官網的例子直接拷貝過來:

<select id="selecUser">
  <bind name="myName" value="'%' + _parameter.getName() + '%'" />
  SELECT * FROM user
  WHERE name LIKE #{myName}
</select>

你們應該大體能知道以上例子的功效,其實就是輔助構建模糊查詢的語句拼接,那有人就好奇了,爲啥不直接拼接語句就好了,爲何還要搞出一個變量,繞一圈呢?

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

我先問一個問題:平時你使用 mysql 都是如何拼接模糊查詢 like 語句的?

select * from user where name like concat('%',#{name},'%')

確實如此,但若是有一天領導跟你說數據庫換成 oracle 了,怎麼辦?上面的語句還能用嗎?明顯用不了,不能這麼寫,由於 oracle 雖然也有 concat 函數,可是隻支持鏈接兩個字符串,例如你最多這麼寫:

select * from user where name like concat('%',#{name})

可是少了右邊的井號符號,因此達不到你預期的效果,因而你改爲這樣:

select * from user where name like '%'||#{name}||'%'

確實能夠了,可是過幾天領導又跟你說,數據庫換回 mysql 了?額… 那很差意思,你又得把相關使用到模糊查詢的地方改回來。

select * from user where name like concat('%',#{name},'%')

很顯然,數據庫只要發生變動你的 sql 語句就得跟着改,特別麻煩,因此纔有了一開始咱們介紹 bind 標籤官網的這個例子,不管使用哪一種數據庫,這個模糊查詢的 Like 語法都是支持的:

<select id="selecUser">
  <bind name="myName" value="'%' + _parameter.getName() + '%'" />
  SELECT * FROM user
  WHERE name LIKE #{myName}
</select>

這個 bind 的用法,實打實解決了數據庫從新選型後致使的一些問題,固然在實際工做中發生的機率不會太大,因此 bind 的使用我我的確實也使用的很少,可能還有其它一些應用場景,但願有人能發現以後來跟咱們分享一下,總之我勉強給了一顆星(雖然沒太多實際用處,但畢竟要給點面子)。


拓展:sql標籤 + include 標籤

經常使用度:★★★☆☆

實用性:★★★☆☆

sql 標籤與 include 標籤組合使用,用於 SQL 語句的複用,平常高頻或公用使用的語句塊能夠抽取出來進行復用,其實咱們應該不陌生,早期咱們學習 JSP 的時候,就有一個 include 標記能夠引入一些公用可複用的頁面文件,例如頁面頭部或尾部頁面代碼元素,這種複用的設計很常見。

嚴格意義上 sql 、include 不算在動態 SQL 標籤成員以內,只因它確實是寶藏般的存在,因此我要簡單說說,sql 標籤用於定義一段可重用的 SQL 語句片斷,以便在其它語句中使用,而 include 標籤則經過屬性 refid 來引用對應 id 匹配的 sql 標籤語句片斷。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

簡單的複用代碼塊能夠是:

<!-- 可複用的字段語句塊 -->
<sql id="userColumns">
    id,username,password 
</sql>

查詢或插入時簡單複用:

<!-- 查詢時簡單複用 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"></include> 
  from user 
</select>

<!-- 插入時簡單複用 -->
<insert id="insertUser" resultType="map">
  insert into user(
    <include refid="userColumns"></include> 
  )values(
    #{id},#{username},#{password} 
  )  
</insert>

固然,複用語句還支持屬性傳遞,例如:

<!-- 可複用的字段語句塊 -->
<sql id="userColumns">
    ${pojo}.id,${pojo}.username 
</sql>

這個 SQL 片斷能夠在其它語句中使用:

<!-- 查詢時複用 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns">
        <property name="pojo" value="u1"/>
    </include>,
    <include refid="userColumns">
        <property name="pojo" value="u2"/>
    </include>
  from user u1 cross join user u2
</select>

也能夠在 include 元素的 refid 屬性或多層內部語句中使用屬性值,屬性能夠穿透傳遞,例如:

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

<!-- 簡單語句塊 -->
<sql id="sql1">
  ${prefix}_user
</sql>

<!-- 嵌套語句塊 -->
<sql id="sql2">
  from
    <include refid="${include_target}"/>
</sql>

<!-- 查詢時引用嵌套語句塊 -->
<select id="select" resultType="map">
  select
    id, username
  <include refid="sql2">
    <property name="prefix" value="t"/>
    <property name="include_target" value="sql1"/>
  </include>
</select>

至此,關於 9 大動態 SQL 標籤的基本用法咱們已介紹完畢,另外咱們還有一些疑問:Mybatis 底層是如何解析這些動態 SQL 標籤的呢?最終又是怎麼構建完整可執行的 SQL 語句的呢?帶着這些疑問,咱們在第4節中詳細分析。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

四、動態SQL的底層原理

想了解 Mybatis 到底是如何解析與構建動態 SQL ?首先推薦的固然是讀源碼,而讀源碼,是一個技術鑽研問題,爲了借鑑學習,爲了工做儲備,爲了解決問題,爲了讓本身在編程的道路上跑得明白一些... 而但願經過讀源碼,去了解底層實現原理,切記不能脫離了總體去讀局部,不然你瞭解到的必然侷限且片面,從而輕忽了真核上的設計。如同咱們讀史或者觀宇宙同樣,最好的辦法都是從總體到局部,不斷放大,先後延展,會很舒服通透。因此我準備從 Mybatis 框架的核心主線上去逐步放大剖析。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

經過前面幾篇文章的介紹(建議閱讀 Mybatis 系列全解之六:《Mybatis 最硬核的 API 你知道幾個?》),其實咱們知道了 Mybatis 框架的核心部分在於構件的構建過程,從而支撐了外部應用程序的使用,從應用程序端建立配置並調用 API 開始,到框架端加載配置並初始化構件,再建立會話並接收請求,而後處理請求,最終返回處理結果等。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

咱們的動態 SQL 解析部分就發生在 SQL 語句對象 MappedStatement 構建時(上左高亮橘色部分,注意觀察其中 SQL 語句對象與 SqlSource 、 BoundSql 的關係,在動態 SQL 解析流程特別關鍵)。咱們再拉近一點,能夠看到不管是使用 XML 配置 SQL 語句或是使用註解方式配置 SQL 語句,框架最終都會把解析完成的 SQL 語句對象存放到 MappedStatement 語句集合池子。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

而以上虛線高亮部分,便是 XML 配置方式解析過程與註解配置方式解析過程當中涉及到動態 SQL 標籤解析的流程,咱們分別講解:

  • 第一,XML 方式配置 SQL 語句,框架如何解析?

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

以上爲 XML 配置方式的 SQL 語句解析過程,不管是單獨使用 Mybatis 框架仍是集成 Spring 與 Mybatis 框架,程序啓動入口都會首先從 SqlSessionFactoryBuilder.build() 開始構建,依次經過 XMLConfigBuilder 構建全局配置 Configuration 對象、經過 XMLMapperBuilder 構建每個 Mapper 映射器、經過 XMLStatementBuilder 構建映射器中的每個 SQL 語句對象(select/insert/update/delete)。而就在解析構建每個 SQL 語句對象時,涉及到一個關鍵的方法 parseStatementNode(),即上圖橘紅色高亮部分,此方法內部就出現了一個處理動態 SQL 的核心節點。

// XML配置語句構建器
public class XMLStatementBuilder {

    // 實際解析每個 SQL 語句
    // 例如 select|insert|update|delete
    public void parseStatementNode() {

        // [忽略]參數構建...
        // [忽略]緩存構建..
        // [忽略]結果集構建等等.. 

        // 【重點】此處便是處理動態 SQL 的核心!!!
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
        SqlSource sqlSource = langDriver.createSqlSource(..);

        // [忽略]最後把解析完成的語句對象添加進語句集合池
        builderAssistant.addMappedStatement(語句對象)
    }
}

你們先重點關注一下這段代碼,其中【重點】部分的 LanguageDriver 與 SqlSource 會是咱們接下來說解動態 SQL 語句解析的核心類,咱們不着急剖析,咱們先把註解方式流程也梳理對比一下。

  • 第二,註解方式配置 SQL 語句,框架如何解析?

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

你們會發現註解配置方式的 SQL 語句解析過程,與 XML 方式極爲相像,惟一不一樣點就在於解析註解 SQL 語句時,使用了 MapperAnnotationBuilder 構建器,其中關於每個語句對象 (@Select,@Insert,@Update,@Delete等) 的解析,又都會經過一個關鍵解析方法 parseStatement(),即上圖橘紅色高亮部分,此方法內部一樣的出現了一個處理動態 SQL 的核心節點。

// 註解配置語句構建器
public class MapperAnnotationBuilder {

    // 實際解析每個 SQL 語句
    // 例如 @Select,@Insert,@Update,@Delete
    void parseStatement(Method method) {  

        // [忽略]參數構建...
        // [忽略]緩存構建..
        // [忽略]結果集構建等等.. 

        // 【重點】此處便是處理動態 SQL 的核心!!!
        final LanguageDriver languageDriver = getLanguageDriver(method);  
        final SqlSource sqlSource = buildSqlSource( languageDriver,... );

        // [忽略]最後把解析完成的語句對象添加進語句集合池
        builderAssistant.addMappedStatement(語句對象)

    }    
}

因而可知,無論是經過 XML 配置語句仍是註解方式配置語句,構建流程都是 大體相同,而且依然出現了咱們在 XML 配置方式中涉及到的語言驅動 LanguageDriver 與語句源 SqlSource ,那這兩個類/接口到底爲什麼物,爲什麼能讓 SQL 語句解析者都如此繞不開 ?

這一切,得從你編寫的 SQL 開始講起 ...

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

咱們知道,不管 XML 仍是註解,最終你的全部 SQL 語句對象都會被齊齊整整的解析完放置在 SQL 語句對象集合池中,以供執行器 Executor 具體執行增刪改查 ( CRUD ) 時使用。而咱們知道每個 SQL 語句對象的屬性,特別複雜繁多,例如超時設置、緩存、語句類型、結果集映射關係等等。

// SQL 語句對象
public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;

  // SQL 源
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;

}

而其中有一個特別的屬性就是咱們的語句源 SqlSource ,功能純粹也恰如其名 SQL 源。它是一個接口,它會結合用戶傳遞的參數對象 parameterObject 與動態 SQL,生成 SQL 語句,並最終封裝成 BoundSql 對象。SqlSource 接口有5個實現類,分別是:StaticSqlSource、DynamicSqlSource、RawSqlSource、ProviderSqlSource、VelocitySqlSource (而 velocitySqlSource 目前只是一個測試用例,尚未用做實際的 Sql 源實現)。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

  • StaticSqlSource:靜態 SQL 源實現類,全部的 SQL 源最終都會構建成 StaticSqlSource 實例,該實現類會生成最終可執行的 SQL 語句供 statement 或 prepareStatement 使用。
  • RawSqlSource:原生 SQL 源實現類,解析構建含有 ‘#{}’ 佔位符的 SQL 語句或原生 SQL 語句,解析完最終會構建 StaticSqlSource 實例。
  • DynamicSqlSource:動態 SQL 源實現類,解析構建含有 ‘${}’ 替換符的 SQL 語句或含有動態 SQL 的語句(例如 If/Where/Foreach等),解析完最終會構建 StaticSqlSource 實例。
  • ProviderSqlSource:註解方式的 SQL 源實現類,會根據 SQL 語句的內容分發給 RawSqlSource 或 DynamicSqlSource ,固然最終也會構建 StaticSqlSource 實例。
  • VelocitySqlSource:模板 SQL 源實現類,目前(V3.5.6)官方申明這只是一個測試用例,尚未用做真正的模板 Sql 源實現類。

SqlSource 實例在配置類 Configuration 解析階段就被建立,Mybatis 框架會依據3個維度的信息來選擇構建哪一種數據源實例:(純屬我我的理解的歸類梳理~)

  • 第一個維度:客戶端的 SQL 配置方式:XML 方式或者註解方式。
  • 第二個維度:SQL 語句中是否使用動態 SQL ( if/where/foreach 等 )。
  • 第三個維度:SQL 語句中是否含有替換符 ‘${}’ 或佔位符 ‘#{}’ 。

SqlSource 接口只有一個方法 getBoundSql ,就是建立 BoundSql 對象。

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

經過 SQL 源就可以獲取 BoundSql 對象,從而獲取最終送往數據庫(經過JDBC)中執行的 SQL 字符串。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

JDBC 中執行的 SQL 字符串,確實就在 BoundSql 對象中。BoundSql 對象存儲了動態(或靜態)生成的 SQL 語句以及相應的參數信息,它是在執行器具體執行 CURD 時經過實際的 SqlSource 實例所構建的。

public class BoundSql { 

  //該字段中記錄了SQL語句,該SQL語句中可能含有"?"佔位符
  private final String sql;

  //SQL中的參數屬性集合
  private final List<ParameterMapping> parameterMappings;

  //客戶端執行SQL時傳入的實際參數值
  private final Object parameterObject;

  //複製 DynamicContext.bindings 集合中的內容
  private final Map<String, Object> additionalParameters;

  //經過 additionalParameters 構建元參數對象
  private final MetaObject metaParameters;

}

在執行器 Executor 實例(例如BaseExecutor)執行增刪改查時,會經過 SqlSource 構建 BoundSql 實例,而後再經過 BoundSql 實例獲取最終輸送至數據庫執行的 SQL 語句,系統可根據 SQL 語句構建 Statement 或者 PrepareStatement ,從而送往數據庫執行,例如語句處理器 StatementHandler 的執行過程。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

牆裂推薦閱讀以前第六文之 Mybatis 最硬核的 API 你知道幾個?這些執行流程都有細講。

到此咱們介紹完 SQL 源 SqlSource 與 BoundSql 的關係,注意 SqlSource 與 BoundSql 不是同個階段產生的,而是分別在程序啓動階段與運行時。

  • 程序啓動初始構建時,框架會根據 SQL 語句類型構建對應的 SqlSource 源實例(靜態/動態).
  • 程序實際運行時,框架會根據傳入參數動態的構建 BoundSql 對象,輸送最終 SQL 到數據庫執行。

在上面咱們知道了 SQL 源是語句對象 BoundSql 的屬性,同時還坐擁5大實現類,那到底是誰建立了 SQL 源呢?其實就是咱們接下來準備介紹的語言驅動 LanguageDriver !

public interface LanguageDriver {
    SqlSource createSqlSource(...);
}

語言驅動接口 LanguageDriver 也是極簡潔,內部定義了構建 SQL 源的方法,LanguageDriver 接口有2個實現類,分別是: XMLLanguageDriver 、 RawLanguageDriver。簡單介紹一下:

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

  • XMLLanguageDriver :是框架默認的語言驅動,可以根據上面咱們講解的 SQL 源的3個維度建立對應匹配的 SQL 源(DynamicSqlSource、RawSqlSource等)。下面這段代碼是 Mybatis 在裝配全局配置時的一些跟語言驅動相關的動做,我摘抄出來,分別有:內置了兩種語言驅動並設置了別名方便引用、註冊了兩種語言驅動至語言註冊工廠、把 XML 語言驅動設置爲默認語言驅動。
// 全局配置的構造方法
public Configuration() {
    // 內置/註冊了不少有意思的【別名】
    // ...

    // 其中就內置了上述的兩種語言驅動【別名】
    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

    // 註冊了XML【語言驅動】 --> 並設置成默認!   
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);

    // 註冊了原生【語言驅動】
    languageRegistry.register(RawLanguageDriver.class);
}
  • RawLanguageDriver :看名字得知是原生語言驅動,事實也如此,它只能建立原生 SQL 源(RawSqlSource),另外它還繼承了 XMLLanguageDriver 。
/**
 * As of 3.2.4 the default XML language is able to identify static statements
 * and create a {@link RawSqlSource}. So there is no need to use RAW unless you
 * want to make sure that there is not any dynamic tag for any reason.
 *
 * @since 3.2.0
 * @author Eduardo Macarron
 */
public class RawLanguageDriver extends XMLLanguageDriver {
}

註釋的大體意思:自 Mybatis 3.2.4 以後的版本, XML 語言驅動就支持解析靜態語句(動態語句固然也支持)並建立對應的 SQL 源(例如靜態語句是原生 SQL 源),因此除非你十分肯定你的 SQL 語句中沒有包含任何一款動態標籤,不然就不要使用 RawLanguageDriver !不然會報錯!!!先看個別名引用的例子:

<select id="findAll"  resultType="map" lang="RAW" >
     select * from user
</select>

<!-- 別名或全限定類名都容許 -->

<select id="findAll"  resultType="map" lang="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver">
     select * from user
</select>

框架容許咱們經過 lang 屬性手工指定語言驅動,不指定則系統默認是 lang = "XML",XML 表明 XMLLanguageDriver ,固然 lang 屬性能夠是咱們內置的別名也能夠是咱們的語言驅動全限定名,不過值得注意的是,當語句中含有動態 SQL 標籤時,就只能選擇使用 lang="XML",不然程序在初始化構件時就會報錯。

## Cause: org.apache.ibatis.builder.BuilderException: 
## Dynamic content is not allowed when using RAW language
## 動態語句內容不被原生語言驅動支持!

這段錯誤提示實際上是發生在 RawLanguageDriver 檢查動態 SQL 源時:

public class RawLanguageDriver extends XMLLanguageDriver { 

  // RAW 不能包含動態內容
  private void checkIsNotDynamic(SqlSource source) {
    if (!RawSqlSource.class.equals(source.getClass())) {
      throw new BuilderException(
          "Dynamic content is not allowed when using RAW language"
      );
    }
  } 
}

至此,基本邏輯咱們已經梳理清楚:程序啓動初始階段,語言驅動建立 SQL 源,而運行時, SQL 源動態解析構建出 BoundSql 。

那麼除了系統默認的兩種語言驅動,還有其它嗎?

答案是:有,例如 Mybatis 框架中目前使用了一個名爲 VelocityLanguageDriver 的語言驅動。相信你們都學習過 JSP 模板引擎,同時還有不少人學習過其它一些(頁面)模板引擎,例如 freemark 和 velocity ,不一樣模板引擎有本身的一套模板語言語法,而其中 Mybatis 就嘗試使用了 Velocity 模板引擎做爲語言驅動,目前雖然 Mybatis 只是在測試用例中使用到,可是它告訴了咱們,框架容許自定義語言驅動,因此不僅是 XML、RAW 兩種語言驅動中使用的 OGNL 語法,也能夠是 Velocity (語法),或者你本身所能定義的一套模板語言(同時你得定義一套語法)。 例如如下就是 Mybatis 框架中使用到的 Velocity 語言驅動和對應的 SQL 源,它們使用 Velocity 語法/方式解析構建 BoundSql 對象。

/**
 * Just a test case. Not a real Velocity implementation.
 * 只是一個測試示例,還不是一個真正的 Velocity 方式實現
 */
public class VelocityLanguageDriver implements LanguageDriver {
     public SqlSource createSqlSource() {...}
}
public class VelocitySqlSource implements SqlSource {
     public BoundSql getBoundSql() {...}
}

好,語言驅動的基本概念大體如此。咱們回過頭再詳細看看動態 SQL 源 SqlSource,做爲語句對象 MappedStatement 的屬性,在 程序初始構建階段,語言驅動是怎麼建立它的呢?不妨咱們先看看經常使用的動態 SQL 源對象是怎麼被建立的吧!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

經過以上的程序初始構建階段,咱們能夠發現,最終語言驅動經過調用 XMLScriptBuilder 對象來建立 SQL 源。

// XML 語言驅動
public class XMLLanguageDriver implements LanguageDriver {  

    // 經過調用 XMLScriptBuilder 對象來建立 SQL 源
    @Override
    public SqlSource createSqlSource() {
        // 實例
        XMLScriptBuilder builder = new XMLScriptBuilder();
        // 解析
        return builder.parseScriptNode();
    }
}

而在前面咱們就已經介紹, XMLScriptBuilder 實例初始構造時,會初始構建全部動態標籤處理器:

// XML腳本標籤構建器
public class XMLScriptBuilder{
    // 標籤節點處理器池
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

    // 構造器
    public XMLScriptBuilder() { 
        initNodeHandlerMap();
        //... 其它初始化不贅述也不重要
    }

    // 動態標籤處理器
    private void initNodeHandlerMap() {
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("where", new WhereHandler());
        nodeHandlerMap.put("set", new SetHandler());
        nodeHandlerMap.put("foreach", new ForEachHandler());
        nodeHandlerMap.put("if", new IfHandler());
        nodeHandlerMap.put("choose", new ChooseHandler());
        nodeHandlerMap.put("when", new IfHandler());
        nodeHandlerMap.put("otherwise", new OtherwiseHandler());
        nodeHandlerMap.put("bind", new BindHandler());
    }
}

繼 XMLScriptBuilder 初始化流程以後,解析建立 SQL 源流程再分爲兩步:

一、解析動態標籤,經過判斷每一塊動態標籤的類型,使用對應的標籤處理器進行解析屬性和語句處理,並最終放置到混合 SQL 節點池中(MixedSqlNode),以供程序運行時構建 BoundSql 時使用。

二、new SQL 源,根據 SQL 是否有動態標籤或通配符佔位符來確認產生對象的靜態或動態 SQL 源。

public SqlSource parseScriptNode() {

    // 一、解析動態標籤 ,並放到混合SQL節點池中
    MixedSqlNode rootSqlNode = parseDynamicTags(context);

    // 二、根據語句類型,new 出來最終的 SQL 源
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

原來解析動態標籤的工做交給了 parseDynamicTags() 方法,而且每個語句對象的動態 SQL 標籤最終都會被放到一個混合 SQL 節點池中。

// 混合 SQL 節點池
public class MixedSqlNode implements SqlNode {

    // 全部動態 SQL 標籤:IF、WHERE、SET 等
    private final List<SqlNode> contents;
}

咱們先看一下 SqlNode 接口的實現類,基本涵蓋了咱們全部動態 SQL 標籤處理器所須要使用到的節點實例。而其中混合 SQL 節點 MixedSqlNode 做用僅是爲了方便獲取每個語句的全部動態標籤節點,因而應勢而生。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

知道動態 SQL 標籤節點處理器及以上的節點實現類以後,其實就能很容易理解,到達程序運行時,執行器會調用 SQL 源來協助構建 BoundSql 對象,而 SQL 源的核心工做,就是根據每一小段標籤類型,匹配到對應的節點實現類以解析拼接每一小段 SQL 語句。

程序運行時,動態 SQL 源獲取 BoundSql 對象 :

// 動態 SQL 源
public class DynamicSqlSource implements SqlSource { 

    // 這裏的 rootSqlNode 屬性就是 MixedSqlNode 
    private final SqlNode rootSqlNode;

    @Override
    public BoundSql getBoundSql(Object parameterObject) {

        // 動態SQL核心解析流程  
        rootSqlNode.apply(...);  

        return boundSql;

    } 
}

很明顯,經過調用 MixedSqlNode 的 apply () 方法,循環遍歷每個具體的標籤節點。

public class MixedSqlNode implements SqlNode {

      // 全部動態 SQL 標籤:IF、WHERE、SET 等
      private final List<SqlNode> contents; 

      @Override
      public boolean apply(...) {

        // 循環遍歷,把每個節點的解析分派到具體的節點實現之上
        // 例如 <if> 節點的解析交給 IfSqlNode
        // 例如 純文本節點的解析交給 StaticTextSqlNode
        contents.forEach(node -> node.apply(...));
        return true;
      }
}

咱們選擇一兩個標籤節點的解析過程進行說明,其它標籤節點實現類的處理也基本雷同。首先咱們看一下 IF 標籤節點的處理:

// IF 標籤節點
public class IfSqlNode implements SqlNode { 

      private final ExpressionEvaluator evaluator;

      // 實現邏輯
      @Override
      public boolean apply(DynamicContext context) {

        // evaluator 是一個基於 OGNL 語法的解析校驗類
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
          contents.apply(context);
          return true;
        }
        return false;
      } 
}

IF 標籤節點的解析過程很是簡單,經過解析校驗類 ExpressionEvaluator 來對 IF 標籤的 test 屬性內的表達式進行解析校驗,知足則拼接,不知足則跳過。咱們再看看 Trim 標籤的節點解析過程,set 標籤與 where 標籤的底層處理都基於此:

public class TrimSqlNode implements SqlNode { 

    // 核心處理方法
    public void applyAll() {

        // 前綴智能補充與去除
        applyPrefix(..); 

        // 前綴智能補充與去除
        applySuffix(..); 
    } 
}

再來看一個純文本標籤節點實現類的解析處理流程:

// 純文本標籤節點實現類
public class StaticTextSqlNode implements SqlNode {

    private final String text;

    public StaticTextSqlNode(String text) {
        this.text = text;
    }

    // 節點處理,僅僅就是純粹的語句拼接
    @Override
    public boolean apply(DynamicContext context) {
        context.appendSql(text);
        return true;
    }
}

到這裏,動態 SQL 的底層解析過程咱們基本講解完,冗長了些,但流程上大體算完整,有遺漏的,咱們回頭再補充。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

總結

不知不覺中,我又是這麼巨篇幅的講解剖析,確實不太適合碎片化時間閱讀,不過話說回來,畢竟此文屬於 Mybatis 全解系列,做爲學研者仍是建議深諳其中,對日後衆多框架技術的學習必有幫助。本文中咱們不少動態 SQL 的介紹基本都使用 XML 配置方式,固然註解方式配置動態 SQL 也是支持的,動態 SQL 的語法書寫同 XML 方式,可是須要在字符串先後添加 script 標籤申明該語句爲動態 SQL ,例如:

public class UserDao {

    /**
     * 更新用戶
     */
    @Select(
        "<script>"+
        "   UPDATE user   "+
        "   <trim prefix=\"SET\" prefixOverrides=\",\"> "+
        "       <if test=\"username != null and username != ''\"> "+
        "           , username = #{username} "+
        "       </if> "+
        "   </trim> "+
        "   where id = ${id}"
        "</script>"
    )
    void updateUser( User user);

}

此種動態 SQL 寫法可讀性較差,而且維護起來也挺硌手,因此我我的是青睞 xml 方式配置語句,一直追求解耦,大道也至簡。固然,也有不少團隊和項目都在使用註解方式開發,這些沒有絕對,仍是得結合本身的實際項目狀況與團隊等去作取捨。

本篇完,本系列下一篇咱們講《 Mybatis系列全解(九):Mybatis的複雜映射 》。

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

文章持續更新,微信搜索「潘潘和他的朋友們」第一時間閱讀,隨時有驚喜。本文會在 GitHub https://github.com/JavaWorld 收錄,關於熱騰騰的技術、框架、面經、解決方案、摸魚技巧、教程、視頻、漫畫等等等等,咱們都會以最美的姿式第一時間送達,歡迎 Star ~ 咱們將來 不止文章!想進讀者羣的朋友歡迎撩我我的號:panshenlian,備註「加羣」咱們羣裏暢聊, BIU ~

Mybatis系列全解(八):Mybatis的9大動態SQL標籤你知道幾個?提早致女神!

相關文章
相關標籤/搜索