Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神!
封面:洛小汐
作者:潘潘
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); // 有 idUser 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 userWHERE 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 userWHERE 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="u1"/></include>,<include refid="userColumns"><property name="pojo" value="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">selectid, username<include refid="sql2"><property name="prefix" value="t"/><property name="include_target" value="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|deletepublic 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,@Deletevoid 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 語言驅動設置為默認語言驅動。
- RawLanguageDriver :看名字得知是原生語言驅動,事實也如此,它只能創建原生 SQL 源(RawSqlSource),另外它還繼承了 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 源@Overridepublic 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;@Overridepublic 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; @Overridepublic boolean apply(...) {// 循環遍歷,把每一個節點的解析分派到具體的節點實現之上// 例如 <if> 節點的解析交給 IfSqlNode// 例如 純文本節點的解析交給 StaticTextSqlNodecontents.forEach(node -> node.apply(...));return true;} }我們選擇一兩個標簽節點的解析過程進行說明,其它標簽節點實現類的處理也基本雷同。首先我們看一下 IF 標簽節點的處理:
// IF 標簽節點 public class IfSqlNode implements SqlNode { private final ExpressionEvaluator evaluator;// 實現邏輯@Overridepublic 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;}// 節點處理,僅僅就是純粹的語句拼接@Overridepublic 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 ~
總結
以上是生活随笔為你收集整理的Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 清理C盘一些缓存
- 下一篇: PostgreSQL逻辑备份pg_dum