封面:洛小汐
作者:潘潘


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


這算是春節后首篇 Mybatis 文了~
跨了個年感覺寫了有半個世紀 ...
借著女神節 ヾ(?°?°?)??
提前祝男神女神們越靚越富越嗨森!
上圖保存可做朋友圈封面圖 ~
前言
本節我們介紹 Mybatis 的強大特性之一:動態 SQL ,從動態 SQL 的誕生背景與基礎概念,到動態 SQL 的標簽成員及基本用法,我們徐徐道來,再結合框架原始碼,剖析動態 SQL (標簽)的底層原理,最終在文末吐槽一下:在無動態 SQL 特性(標簽)之前,我們會常常掉進哪些可惡的坑吧~

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

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原始碼剖析
本文目錄
1、什么是動態SQL
2、動態SQL的誕生記
3、動態SQL標簽的9大標簽
4、動態SQL的底層原理

1、什么是動態SQL ?
關于動態 SQL ,允許我們理解為 “ 動態的 SQL ”,其中 “ 動態的 ” 是形容詞,“ SQL ” 是名詞,那顯然我們需要先理解名詞,畢竟形容詞僅僅代表它的某種形態或者某種狀態,
SQL 的全稱是:
Structured Query Language,結構化查詢語言,
SQL 本身好說,我們小學時候都學習過了,無非就是 CRUD 嘛,而且我們還知道它是一種 語言,語言是一種存在于物件之間用于交流表達的 能力,例如跟中國人交流用漢語、跟英國人交流用英語、跟火星人交流用火星語、跟小貓交流用喵喵語、跟計算機交流我們用機器語言、跟資料庫管理系統(DBMS)交流我們用 SQL,

想必大家立馬就能明白,想要與某個物件交流,必須擁有與此物件交流的語言能力才行!所以無論是技術人員、還是應用程式系統、或是某個高級語言環境,想要訪問/操作資料庫,都必須具備 SQL 這項能力;因此你能看到像 Java ,像 Python ,像 Go 等等這些高級語言環境中,都會嵌入(支持) SQL 能力,達到與資料庫互動的目的,

很顯然,能夠學習 Mybatis 這么一門高精尖(ru-men)持久層框架的編程人群,對于 SQL 的撰寫能力肯定已經掌握得 ss 的,平時各種 SQL 撰寫那都是信手拈來的事, 只不過對于 動態SQL 到底是個什么東西,似憾訓有一些朋友似懂非懂!但是沒關系,我們百度一下,
動態 SQL:一般指根據用戶輸入或外部條件 動態組合 的 SQL 陳述句塊,
很容易理解,隨外部條件動態組合的 SQL 陳述句塊!我們先針對動態 SQL 這個詞來剖析,世間萬物,有動態那就相對應的有靜態,那么他們的邊界在哪里呢?又該怎么區分呢?

其實,上面我們已經介紹過,在例如 Java 高級語言中,都會嵌入(支持)SQL 能力,一般我們可以直接在代碼或組態檔中撰寫 SQL 陳述句,如果一個 SQL 陳述句在 “編譯階段” 就已經能確定 主體結構,那我們稱之為靜態 SQL,如果一個 SQL 陳述句在編譯階段無法確定主體結構,需要等到程式真正 “運行時” 才能最終確定,那么我們稱之為動態 SQL,舉個例子:
<!-- 1、定義SQL -->
<mapper namespace="dao">
<select id="selectAll" resultType="user">
select * from t_user
</select>
</mapper>
// 2、執行SQL
sqlSession.select("dao.selectAll");
很明顯,以上這個 SQL ,在編譯階段我們都已經知道它的主體結構,即查詢 t_user 表的所有記錄,而無需等到程式運行時才確定這個主體結構,因此以上屬于 靜態 SQL,那我們再看看下面這個陳述句:
<!-- 1、定義SQL -->
<mapper namespace="dao">
<select id="selectAll" parameterType="user">
select * from t_user
<if test="id != null">
where id = #{id}
</if>
</select>
</mapper>
// 2、執行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 ,

另外,還有一種常見的情況,大家看看下面這個 SQL 陳述句算是動態 SQL 陳述句嗎?
<!-- 1、定義SQL -->
<mapper namespace="dao">
<select id="selectAll" parameterType="user">
select * from t_user where id = #{id}
</select>
</mapper>
// 2、執行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 獨有的嗎?


2、動態SQL的誕生記
我們都知道,SQL 是一種偉大的資料庫語言 標準,在資料庫管理系統紛爭的時代,它的出現統一規范了資料庫操作語言,而此時,市面上的資料庫管理軟體百花齊放,我最早使用的 SQL Server 資料庫,當時用的資料庫管理工具是 SQL Server Management Studio,后來接觸 Oracle 資料庫,用了 PL/SQL Developer,再后來直至今日就幾乎都在用 MySQL 資料庫(這個跟各種云廠商崛起有關),所以基本使用 Navicat 作為資料庫管理工具,當然如今市面上還有許多許多,資料庫管理工具嘛,只要能便捷高效的管理我們的資料庫,那就是好工具,duck 不必糾結選擇哪一款!

那這么多好工具,都提供什么功能呢?相信我們平時接觸最多的就是接收執行 SQL 陳述句的輸入界面(也稱為查詢編輯器),這個輸入界面幾乎支持所有 SQL 語法,例如我們撰寫一條陳述句查詢 id 等于15 的用戶資料記錄:
select * from user where id = 15 ;
我們來看一下這個查詢結果:

很顯然,在這個輸入界面內輸入的任何 SQL 陳述句,對于資料庫管理工具來說,都是 動態 SQL!因為工具本身并不可能提前知道用戶會輸入什么 SQL 陳述句,只有當用戶執行之后,工具才接收到用戶實際輸入的 SQL 陳述句,才能最終確定 SQL 陳述句的主體結構,當然!即使我們不通過可視化的資料庫管理工具,也可以用資料庫本身自帶支持的命令列工具來執行 SQL 陳述句,但無論用戶使用哪類工具,輸入的陳述句都會被工具認為是 動態 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 借助 OGNL 的運算式的偉大設計,可算在動態 SQL 構建方面提供了各類功能強大的輔助標簽,我們簡單列舉一下有:if、choose、when、otherwise、trim、where、set、foreach、bind等,我隨手翻了翻我電腦里頭曾經保存的學習筆記,我們一起在第3節中溫故知新,詳細的講一講吧~

另外,需要糾正一點,就是我們平日里在 Mybatis 框架中常說的動態 SQL ,其實特指的也就是 Mybatis 框架中的這一套動態 SQL 標簽,或者說是這一 特性,而并不是在說動態 SQL 本身,

3、動態SQL標簽的9大標簽
很好,可算進入我們動態 SQL 標簽的主題,根據前面的鋪墊,其實我們都能發現,很多時候靜態 SQL 陳述句并不能滿足我們復雜的業務場景需求,所以我們需要有適當靈活的一套方式或者能力,來便捷高效的構建動態 SQL 陳述句,去匹配我們動態變化的業務需求,舉個栗子,在下面此類多條件的場景需求之下,動態 SQL 陳述句就顯得尤為重要(先登場 if 標簽),

當然,很多朋友會說這類需求,不能用 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):
Top1、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 的維護上,既靈活也方便維護,可讀性還強,

有些心細的朋友可能就發現一個問題,為什么 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 標簽,它就能滿足多分支判斷的應用場景,
Top2、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>

很明顯,choose 標簽作為多分支條件判斷,提供了更多靈活的流程控制,同時 otherwise 的出現也為程式流程控制兜底,有時能夠避免部分系統風險、過濾部分條件、避免當程式沒有匹配到條件時,把整個資料庫資源全部查詢或更新,
至于為何 choose 標簽這么棒棒,而常用度還是比 if 標簽少了一顆星呢?原因也簡單,因為 choose 標簽的很多使用場景可以直接用 if 標簽代替,另外據我統計,if 標簽在實際業務應用當中,也要多于 choose 標簽,大家也可以具體核查自己的應用程式中動態 SQL 標簽的占比情況,統計分析一下,
Top3、foreach 標簽
常用度:★★★☆☆
實用性:★★★★☆
有些場景,可能需要查詢 id 在 1 ~ 100 的用戶記錄
有些場景,可能需要批量插入 100 條用戶記錄
有些場景,可能需要更新 500 個用戶的姓名
有些場景,可能需要你洗掉 10 條用戶記錄
請問大家:
很多增刪改查場景,操作物件都是集合/串列
如果是你來設計支持 Mybatis 的這一類集合/串列遍歷場景,你會提供什么能力的標簽來輔助構建你的 SQL 陳述句從而去滿足此類業務場景呢?

額(⊙o⊙)…
那如果一定要用 Mybatis 框架呢?

沒錯,確實 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>

可能你會覺得 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>

延伸:當傳入引數為多個物件時,例如傳入 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>
更多玩法,期待你自己去挖掘!

注意:使用 foreach 標簽時,需要對傳入的 collection 引數(List/Map/Set等)進行為空性判斷,否則動態 SQL 會出現語法例外,例如你的查詢陳述句可能是 select * from user where ids in () ,導致以上語法例外就是傳入引數為空,解決方案可以用 if 標簽或 choose 標簽進行為空性判斷處理,或者直接在 Java 代碼中進行邏輯處理即可,例如判斷為空則不執行 SQL ,
Top4、where 標簽、set 標簽
常用度:★★☆☆☆
實用性:★★★★☆
我們把 where 標簽和 set 標簽放置一起講解,一是這兩個標簽在實際應用開發中常用度確實不分伯仲,二是這兩個標簽出自一家,都繼承了 trim 標簽,放置一起方便我們比對追根,(其中底層原理會在第4部分詳細講解)

之前我們介紹 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 標簽也會將它替換去除,

了解了基本用法之后,我們再看看剛剛我們的例子中:
<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 子句元素沒能被成功替換掉前綴,從而引起語法錯誤!

基于 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 標簽都會將它替換去除,

根據此用法我們可以把以上的例子改為:
<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 標簽當成末尾子句元素處理,導致后續真正的末尾子句元素的逗號沒能被成功替換掉后綴,從而引起語法錯誤!

到此,我們的 where 標簽元素與 set 標簽就基本介紹完成,它倆確實極為相似,區別僅在于:
- where 標簽插入前綴 where
- set 標簽插入前綴 set
- where 標簽僅智能替換前綴 AND 或 OR
- set 標簽可以只能替換前綴逗號,或后綴逗號,
而這兩者的前后綴去除策略,都源自于 trim 標簽的設計,我們一起看看到底 trim 標簽是有多靈活!
Top5、trim 標簽
常用度:★☆☆☆☆
實用性:★☆☆☆☆
上面我們介紹了 where 標簽與 set 標簽,它倆的共同點無非就是前置關鍵詞 where 或 set 的插入,以及前后綴符號(例如 AND | OR | ,)的智能去除,基于 where 標簽和 set 標簽本身都繼承了 trim 標簽,所以 trim 標簽的大致實作我們也能猜出個一二三,

其實 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);
}
}
Top6、bind 標簽
常用度:☆☆☆☆☆
實用性:★☆☆☆☆
簡單來說,這個標簽就是可以創建一個變數,并系結到背景關系,即供背景關系使用,就是這樣,我把官網的例子直接拷貝過來:
<select id="selecUser">
<bind name="myName" value="'%' + _parameter.getName() + '%'" />
SELECT * FROM user
WHERE name LIKE #{myName}
</select>
大家應該大致能知道以上例子的功效,其實就是輔助構建模糊查詢的陳述句拼接,那有人就好奇了,為啥不直接拼接陳述句就行了,為什么還要搞出一個變數,繞一圈呢?

我先問一個問題:平時你使用 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 標簽陳述句片段,

簡單的復用代碼塊可以是:
<!-- 可復用的欄位陳述句塊 -->
<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="https://www.cnblogs.com/panshenlian/p/u1"/>
</include>,
<include refid="userColumns">
<property name="pojo" value="https://www.cnblogs.com/panshenlian/p/u2"/>
</include>
from user u1 cross join user u2
</select>
也可以在 include 元素的 refid 屬性或多層內部陳述句中使用屬性值,屬性可以穿透傳遞,例如:

<!-- 簡單陳述句塊 -->
<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="https://www.cnblogs.com/panshenlian/p/t"/>
<property name="include_target" value="https://www.cnblogs.com/panshenlian/p/sql1"/>
</include>
</select>
至此,關于 9 大動態 SQL 標簽的基本用法我們已介紹完畢,另外我們還有一些疑問:Mybatis 底層是如何決議這些動態 SQL 標簽的呢?最終又是怎么構建完整可執行的 SQL 陳述句的呢?帶著這些疑問,我們在第4節中詳細分析,


4、動態SQL的底層原理
想了解 Mybatis 究竟是如何決議與構建動態 SQL ?首先推薦的當然是讀原始碼,而讀原始碼,是一個技術鉆研問題,為了借鑒學習,為了作業儲備,為了解決問題,為了讓自己在編程的道路上跑得明白一些... 而希望通過讀原始碼,去了解底層實作原理,切記不能脫離了整體去讀區域,否則你了解到的必然局限且片面,從而輕忽了真核上的設計,如同我們讀史或者觀宇宙一樣,最好的辦法都是從整體到區域,不斷放大,前后延展,會很舒服通透,所以我準備從 Mybatis 框架的核心主線上去逐步放大剖析,

通過前面幾篇文章的介紹(建議閱讀 Mybatis 系列全解之六:《Mybatis 最硬核的 API 你知道幾個?》),其實我們知道了 Mybatis 框架的核心部分在于構件的構建程序,從而支撐了外部應用程式的使用,從應用程式端創建配置并呼叫 API 開始,到框架端加載配置并初始化構件,再創建會話并接收請求,然后處理請求,最侄訓傳處理結果等,

我們的動態 SQL 決議部分就發生在 SQL 陳述句物件 MappedStatement 構建時(上左高亮橘色部分,注意觀察其中 SQL 陳述句物件與 SqlSource 、 BoundSql 的關系,在動態 SQL 決議流程特別關鍵),我們再拉近一點,可以看到無論是使用 XML 配置 SQL 陳述句或是使用注解方式配置 SQL 陳述句,框架最終都會把決議完成的 SQL 陳述句物件存放到 MappedStatement 陳述句集合池子,

而以上虛線高亮部分,即是 XML 配置方式決議程序與注解配置方式決議程序中涉及到動態 SQL 標簽決議的流程,我們分別講解:
- 第一,XML 方式配置 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 陳述句,框架如何決議?

大家會發現注解配置方式的 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 開始講起 ...

我們知道,無論 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 源實作),

- 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 字串,

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 最硬核的 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,簡單介紹一下:

- 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 源物件是怎么被創建的吧!

通過以上的程式初始構建階段,我們可以發現,最終語言驅動通過呼叫 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 源流程再分為兩步:
1、決議動態標簽,通過判斷每一塊動態標簽的型別,使用對應的標簽處理器進行決議屬性和陳述句處理,并最終放置到混合 SQL 節點池中(MixedSqlNode),以供程式運行時構建 BoundSql 時使用,
2、new SQL 源,根據 SQL 是否有動態標簽或通配符占位符來確認產生物件的靜態或動態 SQL 源,
public SqlSource parseScriptNode() {
// 1、決議動態標簽 ,并放到混合SQL節點池中
MixedSqlNode rootSqlNode = parseDynamicTags(context);
// 2、根據陳述句型別,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 作用僅是為了方便獲取每一個陳述句的所有動態標簽節點,于是應勢而生,

知道動態 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 全解系列,作為學研者還是建議深諳其中,對往后眾多框架技術的學習必有幫助,本文中我們很多動態 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的復雜映射 》,


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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/266210.html
標籤:Java
上一篇:Java this 的使用問題
