封面:洛小汐前端
做者:潘潘java
2021年,仰望天空,腳踏實地。node
這算是春節後首篇 Mybatis 文了~ mysql
跨了個年感受寫了有半個世紀 ... android
藉着女神節 ヾ(◍°∇°◍)ノ゙git
提早祝男神女神們越靚越富越嗨森!github
上圖保存可作朋友圈封面圖 ~sql
本節咱們介紹 Mybatis 的強大特性之一:動態 SQL ,從動態 SQL 的誕生背景與基礎概念,到動態 SQL 的標籤成員及基本用法,咱們徐徐道來,再結合框架源碼,剖析動態 SQL (標籤)的底層原理,最終在文末吐槽一下:在無動態 SQL 特性(標籤)以前,咱們會經常掉進哪些可惡的坑吧~數據庫
建議關注咱們! Mybatis 全解系列一直在更新哦apache
一、什麼是動態SQL
二、動態SQL的誕生記
三、動態SQL標籤的9大標籤
四、動態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,舉個例子:
<!-- 一、定義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 。
另外,還有一種常見的狀況,你們看看下面這個 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 獨有的嗎?
咱們都知道,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 自己。
很好,可算進入咱們動態 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):
經常使用度:★★★★★
實用性:★★★★☆
if 標籤,絕對算得上是一個偉大的標籤,任何不支持流程控制(或語句控制)的應用程序,都是耍流氓,幾乎都不具有現實意義,實際的應用場景和流程必然存在條件的控制與流轉,而 if 標籤在 單條件分支判斷 應用場景中就起到了捨我其誰的做用,語法很簡單,若是知足,則執行,不知足,則忽略/跳過。
舉個例子:
<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 標籤,它就能知足多分支判斷的應用場景。
經常使用度:★★★★☆
實用性:★★★★☆
有些時候,咱們並不但願條件控制是非此即彼的,而是但願能提供多個條件並從中選擇一個,因此貼心的 Mybatis 提供了 choose 標籤元素,相似咱們 Java 當中的 if else 或 switch case default,choose 標籤必須搭配 when 標籤和 otherwise 標籤使用,驗證條件依然是使用 test 屬性進行驗證。
依據下面的例子,當應用程序輸入年齡 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 標籤的佔比狀況,統計分析一下。
經常使用度:★★★☆☆
實用性:★★★★☆
有些場景,可能須要查詢 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 標籤元素的基本用法:
第一,當傳入的參數爲 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 集合中,所以:
所以,若是是 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 。
經常使用度:★★☆☆☆
實用性:★★★★☆
咱們把 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 框架都會幫你智能處理。
用法特別簡單,咱們用官術總結一下:
瞭解了基本用法以後,咱們再看看剛剛咱們的例子中:
<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 標籤元素類似:
根據此用法咱們能夠把以上的例子改成:
<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 標籤就基本介紹完成,它倆確實極爲類似,區別僅在於:
而這二者的先後綴去除策略,都源自於 trim 標籤的設計,咱們一塊兒看看到底 trim 標籤是有多靈活!
經常使用度:★☆☆☆☆
實用性:★☆☆☆☆
上面咱們介紹了 where 標籤與 set 標籤,它倆的共同點無非就是前置關鍵詞 where 或 set 的插入,以及先後綴符號(例如 AND | OR | ,)的智能去除。基於 where 標籤和 set 標籤自己都繼承了 trim 標籤,因此 trim 標籤的大體實現咱們也能猜出個一二三。
其實 where 標籤和 set 標籤都只是 trim 標籤的某種實現方案,trim 標籤底層是經過 TrimSqlNode 類來實現的,它有幾個關鍵屬性:
因此 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); } }
經常使用度:☆☆☆☆☆
實用性:★☆☆☆☆
簡單來講,這個標籤就是能夠建立一個變量,並綁定到上下文,即供上下文使用,就是這樣,我把官網的例子直接拷貝過來:
<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 語句的複用,平常高頻或公用使用的語句塊能夠抽取出來進行復用,其實咱們應該不陌生,早期咱們學習 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"> 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 到底是如何解析與構建動態 SQL ?首先推薦的固然是讀源碼,而讀源碼,是一個技術鑽研問題,爲了借鑑學習,爲了工做儲備,爲了解決問題,爲了讓本身在編程的道路上跑得明白一些... 而但願經過讀源碼,去了解底層實現原理,切記不能脫離了總體去讀局部,不然你瞭解到的必然侷限且片面,從而輕忽了真核上的設計。如同咱們讀史或者觀宇宙同樣,最好的辦法都是從總體到局部,不斷放大,先後延展,會很舒服通透。因此我準備從 Mybatis 框架的核心主線上去逐步放大剖析。
經過前面幾篇文章的介紹(建議閱讀 Mybatis 系列全解之六:《Mybatis 最硬核的 API 你知道幾個?》),其實咱們知道了 Mybatis 框架的核心部分在於構件的構建過程,從而支撐了外部應用程序的使用,從應用程序端建立配置並調用 API 開始,到框架端加載配置並初始化構件,再建立會話並接收請求,而後處理請求,最終返回處理結果等。
咱們的動態 SQL 解析部分就發生在 SQL 語句對象 MappedStatement 構建時(上左高亮橘色部分,注意觀察其中 SQL 語句對象與 SqlSource 、 BoundSql 的關係,在動態 SQL 解析流程特別關鍵)。咱們再拉近一點,能夠看到不管是使用 XML 配置 SQL 語句或是使用註解方式配置 SQL 語句,框架最終都會把解析完成的 SQL 語句對象存放到 MappedStatement 語句集合池子。
而以上虛線高亮部分,便是 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 語句解析過程,與 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 源實現)。
SqlSource 實例在配置類 Configuration 解析階段就被建立,Mybatis 框架會依據3個維度的信息來選擇構建哪一種數據源實例:(純屬我我的理解的歸類梳理~)
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 源是語句對象 BoundSql 的屬性,同時還坐擁5大實現類,那到底是誰建立了 SQL 源呢?其實就是咱們接下來準備介紹的語言驅動 LanguageDriver !
public interface LanguageDriver { SqlSource createSqlSource(...); }
語言驅動接口 LanguageDriver 也是極簡潔,內部定義了構建 SQL 源的方法,LanguageDriver 接口有2個實現類,分別是: XMLLanguageDriver 、 RawLanguageDriver。簡單介紹一下:
// 全局配置的構造方法 public Configuration() { // 內置/註冊了不少有意思的【別名】 // ... // 其中就內置了上述的兩種語言驅動【別名】 typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); // 註冊了XML【語言驅動】 --> 並設置成默認! languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); // 註冊了原生【語言驅動】 languageRegistry.register(RawLanguageDriver.class); }
/** * 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 源流程再分爲兩步:
一、解析動態標籤,經過判斷每一塊動態標籤的類型,使用對應的標籤處理器進行解析屬性和語句處理,並最終放置到混合 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 做用僅是爲了方便獲取每個語句的全部動態標籤節點,因而應勢而生。
知道動態 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 ~