Mybatis系列全解(八):Mybatis的9大動(dòng)態(tài)SQL標(biāo)簽?zāi)阒缼讉€(gè)帝牡?提前致女神!

封面:洛小汐

作者:潘潘

image
image

2021年蒙揣,仰望天空靶溜,腳踏實(shí)地。

image
image

這算是春節(jié)后首篇 Mybatis 文了~

跨了個(gè)年感覺寫了有半個(gè)世紀(jì) ...

借著女神節(jié) ヾ(?°?°?)??

提前祝男神女神們越靚越富越嗨森懒震!

上圖保存可做朋友圈封面圖 ~

前言

本節(jié)我們介紹 Mybatis 的強(qiáng)大特性之一:動(dòng)態(tài) SQL 罩息,從動(dòng)態(tài) SQL 的誕生背景與基礎(chǔ)概念,到動(dòng)態(tài) SQL 的標(biāo)簽成員及基本用法个扰,我們徐徐道來瓷炮,再結(jié)合框架源碼,剖析動(dòng)態(tài) SQL (標(biāo)簽)的底層原理递宅,最終在文末吐槽一下:在無動(dòng)態(tài) SQL 特性(標(biāo)簽)之前娘香,我們會常常掉進(jìn)哪些可惡的坑吧~

image

建議關(guān)注我們! Mybatis 全解系列一直在更新哦

image

Mybaits系列全解

  • Mybatis系列全解(一):手寫一套持久層框架
  • Mybatis系列全解(二):Mybatis簡介與環(huán)境搭建
  • Mybatis系列全解(三):Mybatis簡單CRUD使用介紹
  • Mybatis系列全解(四):全網(wǎng)最全办龄!Mybatis配置文件XML全貌詳解
  • Mybatis系列全解(五):全網(wǎng)最全烘绽!詳解Mybatis的Mapper映射文件
  • Mybatis系列全解(六):Mybatis最硬核的API你知道幾個(gè)?
  • Mybatis系列全解(七):Dao層的兩種實(shí)現(xiàn)之傳統(tǒng)與代理
  • Mybatis系列全解(八):Mybatis的動(dòng)態(tài)SQL
  • Mybatis系列全解(九):Mybatis的復(fù)雜映射
  • Mybatis系列全解(十):Mybatis注解開發(fā)
  • Mybatis系列全解(十一):Mybatis緩存全解
  • Mybatis系列全解(十二):Mybatis插件開發(fā)
  • Mybatis系列全解(十三):Mybatis代碼生成器
  • Mybatis系列全解(十四):Spring集成Mybatis
  • Mybatis系列全解(十五):SpringBoot集成Mybatis
  • Mybatis系列全解(十六):Mybatis源碼剖析

本文目錄

1俐填、什么是動(dòng)態(tài)SQL

2安接、動(dòng)態(tài)SQL的誕生記

3、動(dòng)態(tài)SQL標(biāo)簽的9大標(biāo)簽

4英融、動(dòng)態(tài)SQL的底層原理

image

1盏檐、什么是動(dòng)態(tài)SQL ?

關(guān)于動(dòng)態(tài) SQL 驶悟,允許我們理解為 “ 動(dòng)態(tài)的 SQL ”糯笙,其中 “ 動(dòng)態(tài)的 ” 是形容詞,“ SQL ” 是名詞撩银,那顯然我們需要先理解名詞给涕,畢竟形容詞僅僅代表它的某種形態(tài)或者某種狀態(tài)。

SQL 的全稱是:

Structured Query Language额获,結(jié)構(gòu)化查詢語言够庙。

SQL 本身好說,我們小學(xué)時(shí)候都學(xué)習(xí)過了抄邀,無非就是 CRUD 嘛耘眨,而且我們還知道它是一種 語言,語言是一種存在于對象之間用于交流表達(dá)的 能力境肾,例如跟中國人交流用漢語剔难、跟英國人交流用英語胆屿、跟火星人交流用火星語、跟小貓交流用喵喵語偶宫、跟計(jì)算機(jī)交流我們用機(jī)器語言非迹、跟數(shù)據(jù)庫管理系統(tǒng)(DBMS)交流我們用 SQL。

image

想必大家立馬就能明白纯趋,想要與某個(gè)對象交流憎兽,必須擁有與此對象交流的語言能力才行!所以無論是技術(shù)人員吵冒、還是應(yīng)用程序系統(tǒng)纯命、或是某個(gè)高級語言環(huán)境,想要訪問/操作數(shù)據(jù)庫痹栖,都必須具備 SQL 這項(xiàng)能力亿汞;因此你能看到像 Java ,像 Python 揪阿,像 Go 等等這些高級語言環(huán)境中疗我,都會嵌入(支持) SQL 能力,達(dá)到與數(shù)據(jù)庫交互的目的图甜。

image

很顯然,能夠?qū)W習(xí) Mybatis 這么一門高精尖(ru-men)持久層框架的編程人群鳖眼,對于 SQL 的編寫能力肯定已經(jīng)掌握得 ss 的黑毅,平時(shí)各種 SQL 編寫那都是信手拈來的事, 只不過對于 動(dòng)態(tài)SQL 到底是個(gè)什么東西钦讳,似乎還有一些朋友似懂非懂矿瘦!但是沒關(guān)系,我們百度一下愿卒。

動(dòng)態(tài) SQL:一般指根據(jù)用戶輸入或外部條件 動(dòng)態(tài)組合 的 SQL 語句塊缚去。

很容易理解,隨外部條件動(dòng)態(tài)組合的 SQL 語句塊琼开!我們先針對動(dòng)態(tài) SQL 這個(gè)詞來剖析易结,世間萬物,有動(dòng)態(tài)那就相對應(yīng)的有靜態(tài)柜候,那么他們的邊界在哪里呢搞动?又該怎么區(qū)分呢?

image

其實(shí)渣刷,上面我們已經(jīng)介紹過鹦肿,在例如 Java 高級語言中,都會嵌入(支持)SQL 能力辅柴,一般我們可以直接在代碼或配置文件中編寫 SQL 語句箩溃,如果一個(gè) SQL 語句在 “編譯階段” 就已經(jīng)能確定 主體結(jié)構(gòu)瞭吃,那我們稱之為靜態(tài) SQL,如果一個(gè) SQL 語句在編譯階段無法確定主體結(jié)構(gòu)涣旨,需要等到程序真正 “運(yùn)行時(shí)” 才能最終確定歪架,那么我們稱之為動(dòng)態(tài) SQL,舉個(gè)例子:

<!-- 1开泽、定義SQL -->
<mapper namespace="dao">
    <select id="selectAll" resultType="user">
        select * from t_user
    </select>
</mapper>
// 2牡拇、執(zhí)行SQL
sqlSession.select("dao.selectAll");

很明顯,以上這個(gè) SQL 穆律,在編譯階段我們都已經(jīng)知道它的主體結(jié)構(gòu)惠呼,即查詢 t_user 表的所有記錄,而無需等到程序運(yùn)行時(shí)才確定這個(gè)主體結(jié)構(gòu)峦耘,因此以上屬于 靜態(tài) SQL剔蹋。那我們再看看下面這個(gè)語句:

<!-- 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辅髓、執(zhí)行SQL
User user1 = new User();
user1.setId(1);
sqlSession.select("dao.selectAll",user1);  // 有 id

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

認(rèn)真觀察泣崩,以上這個(gè) SQL 語句,額外添加了一塊 if 標(biāo)簽 作為條件判斷洛口,所以應(yīng)用程序在編譯階段是無法確定 SQL 語句最終主體結(jié)構(gòu)的矫付,只有在運(yùn)行時(shí)根據(jù)應(yīng)用程序是否傳入 id 這個(gè)條件,來動(dòng)態(tài)的拼接最終執(zhí)行的 SQL 語句第焰,因此屬于動(dòng)態(tài) SQL 买优。

image

另外,還有一種常見的情況挺举,大家看看下面這個(gè) SQL 語句算是動(dòng)態(tài) SQL 語句嗎杀赢?

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

根據(jù)動(dòng)態(tài) SQL 的定義脂崔,大家是否能判斷以上的語句塊是否屬于動(dòng)態(tài) SQL?

答案:不屬于動(dòng)態(tài) SQL 梧喷!

原因很簡單砌左,這個(gè) SQL 在編譯階段就已經(jīng)明確主體結(jié)構(gòu)了,雖然外部動(dòng)態(tài)的傳入一個(gè) id 铺敌,可能是1绊困,可能是2,可能是100适刀,但是因?yàn)樗闹黧w結(jié)構(gòu)已經(jīng)確定秤朗,這個(gè)語句就是查詢一個(gè)指定 id 的用戶記錄,它最終執(zhí)行的 SQL 語句不會有任何動(dòng)態(tài)的變化笔喉,所以頂多算是一個(gè)支持動(dòng)態(tài)傳參的靜態(tài) SQL 取视。

至此硝皂,我們對于動(dòng)態(tài) SQL 和靜態(tài) SQL 的區(qū)別已經(jīng)有了一個(gè)基礎(chǔ)認(rèn)知,但是有些好奇的朋友又會思考另一個(gè)問題:動(dòng)態(tài) SQL 是 Mybatis 獨(dú)有的嗎作谭?

image
image

2稽物、動(dòng)態(tài)SQL的誕生記

我們都知道,SQL 是一種偉大的數(shù)據(jù)庫語言 標(biāo)準(zhǔn)折欠,在數(shù)據(jù)庫管理系統(tǒng)紛爭的時(shí)代贝或,它的出現(xiàn)統(tǒng)一規(guī)范了數(shù)據(jù)庫操作語言,而此時(shí)锐秦,市面上的數(shù)據(jù)庫管理軟件百花齊放咪奖,我最早使用的 SQL Server 數(shù)據(jù)庫,當(dāng)時(shí)用的數(shù)據(jù)庫管理工具是 SQL Server Management Studio酱床,后來接觸 Oracle 數(shù)據(jù)庫羊赵,用了 PL/SQL Developer,再后來直至今日就幾乎都在用 MySQL 數(shù)據(jù)庫(這個(gè)跟各種云廠商崛起有關(guān))扇谣,所以基本使用 Navicat 作為數(shù)據(jù)庫管理工具昧捷,當(dāng)然如今市面上還有許多許多,數(shù)據(jù)庫管理工具嘛罐寨,只要能便捷高效的管理我們的數(shù)據(jù)庫靡挥,那就是好工具,duck 不必糾結(jié)選擇哪一款鸯绿!

image

那這么多好工具跋破,都提供什么功能呢?相信我們平時(shí)接觸最多的就是接收執(zhí)行 SQL 語句的輸入界面(也稱為查詢編輯器)楞慈,這個(gè)輸入界面幾乎支持所有 SQL 語法幔烛,例如我們編寫一條語句查詢 id 等于15 的用戶數(shù)據(jù)記錄:

select * from user where id = 15 ;

我們來看一下這個(gè)查詢結(jié)果:

image

很顯然啃擦,在這個(gè)輸入界面內(nèi)輸入的任何 SQL 語句囊蓝,對于數(shù)據(jù)庫管理工具來說,都是 動(dòng)態(tài) SQL令蛉!因?yàn)楣ぞ弑旧聿⒉豢赡芴崆爸烙脩魰斎胧裁?SQL 語句聚霜,只有當(dāng)用戶執(zhí)行之后,工具才接收到用戶實(shí)際輸入的 SQL 語句珠叔,才能最終確定 SQL 語句的主體結(jié)構(gòu)蝎宇,當(dāng)然!即使我們不通過可視化的數(shù)據(jù)庫管理工具祷安,也可以用數(shù)據(jù)庫本身自帶支持的命令行工具來執(zhí)行 SQL 語句姥芥。但無論用戶使用哪類工具,輸入的語句都會被工具認(rèn)為是 動(dòng)態(tài) SQL汇鞭!

image

這么一說凉唐,動(dòng)態(tài) SQL 原來不是 Mybatis 獨(dú)有的特性庸追!其實(shí)除了以上介紹的數(shù)據(jù)庫管理工具以外,在純 JDBC 時(shí)代台囱,我們就經(jīng)常通過字符串來動(dòng)態(tài)的拼接 SQL 語句淡溯,這也是在高級語言環(huán)境(例如 Java 語言編程環(huán)境)中早期常用的動(dòng)態(tài) SQL 構(gòu)建方式!

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

// 動(dòng)態(tài)拼接SQL
StringBuilder sql = new StringBuilder();
sql.append(" select  *   ");
sql.append("   from user ");

// 根據(jù)外部條件id動(dòng)態(tài)拼接SQL
if ( null != id ){
    sql.append(" where id = " + id);
}

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

只不過簿训,這種構(gòu)建動(dòng)態(tài) SQL 的方式咱娶,存在很大的安全問題和異常風(fēng)險(xiǎn)(我們第5點(diǎn)會詳細(xì)介紹),所以不建議使用强品,后來 Mybatis 入世之后膘侮,在對待動(dòng)態(tài) SQL 這件事上,就格外上心择懂,它默默發(fā)誓喻喳,一定要為使用 Mybatis 框架的用戶提供一套棒棒的方案(標(biāo)簽)來靈活構(gòu)建動(dòng)態(tài) SQL!

image

于是乎困曙,Mybatis 借助 OGNL 的表達(dá)式的偉大設(shè)計(jì)表伦,可算在動(dòng)態(tài) SQL 構(gòu)建方面提供了各類功能強(qiáng)大的輔助標(biāo)簽,我們簡單列舉一下有:if慷丽、choose蹦哼、when、otherwise要糊、trim纲熏、where、set锄俄、foreach局劲、bind等,我隨手翻了翻我電腦里頭曾經(jīng)保存的學(xué)習(xí)筆記奶赠,我們一起在第3節(jié)中溫故知新鱼填,詳細(xì)的講一講吧~

image

另外,需要糾正一點(diǎn)毅戈,就是我們平日里在 Mybatis 框架中常說的動(dòng)態(tài) SQL 苹丸,其實(shí)特指的也就是 Mybatis 框架中的這一套動(dòng)態(tài) SQL 標(biāo)簽,或者說是這一 特性苇经,而并不是在說動(dòng)態(tài) SQL 本身赘理。

image

3、動(dòng)態(tài)SQL標(biāo)簽的9大標(biāo)簽

很好扇单,可算進(jìn)入我們動(dòng)態(tài) SQL 標(biāo)簽的主題商模,根據(jù)前面的鋪墊,其實(shí)我們都能發(fā)現(xiàn),很多時(shí)候靜態(tài) SQL 語句并不能滿足我們復(fù)雜的業(yè)務(wù)場景需求施流,所以我們需要有適當(dāng)靈活的一套方式或者能力凉倚,來便捷高效的構(gòu)建動(dòng)態(tài) SQL 語句,去匹配我們動(dòng)態(tài)變化的業(yè)務(wù)需求嫂沉。舉個(gè)栗子稽寒,在下面此類多條件的場景需求之下,動(dòng)態(tài) SQL 語句就顯得尤為重要(先登場 if 標(biāo)簽)趟章。

image

當(dāng)然杏糙,很多朋友會說這類需求,不能用 SQL 來查蚓土,得用搜索引擎宏侍,確實(shí)如此。但是呢蜀漆,在我們的實(shí)際業(yè)務(wù)需求當(dāng)中谅河,還是存在很多沒有引入搜索引擎系統(tǒng),或者有些根本無需引入搜索引擎的應(yīng)用程序或功能确丢,它們也會涉及到多選項(xiàng)多條件或者多結(jié)果的業(yè)務(wù)需求绷耍,那此時(shí)也就確實(shí)需要使用動(dòng)態(tài) SQL 標(biāo)簽來靈活構(gòu)建執(zhí)行語句。

那么鲜侥, Mybatis 目前都提供了哪些棒棒的動(dòng)態(tài) SQL 標(biāo)簽?zāi)?褂始?我們先引出一個(gè)類叫做 XMLScriptBuilder ,大家先簡單理解它是負(fù)責(zé)解析我們的動(dòng)態(tài) SQL 標(biāo)簽的這么一個(gè)構(gòu)建器描函,在第4點(diǎn)底層原理中我們再詳細(xì)介紹崎苗。

// XML腳本標(biāo)簽構(gòu)建器
public class XMLScriptBuilder{
    
    // 標(biāo)簽節(jié)點(diǎn)處理器池
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
    
    // 構(gòu)造器
    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());
    }
}

其實(shí)源碼中很清晰得體現(xiàn),一共有 9 大動(dòng)態(tài) SQL 標(biāo)簽舀寓!Mybatis 在初始化解析配置文件的時(shí)候位仁,會實(shí)例化這么一個(gè)標(biāo)簽節(jié)點(diǎn)的構(gòu)造器买决,那么它本身就會提前把所有 Mybatis 支持的動(dòng)態(tài) SQL 標(biāo)簽對象對應(yīng)的處理器給進(jìn)行一個(gè)實(shí)例化漱挎,然后放到一個(gè) Map 池子里頭鹃锈,而這些處理器书幕,都是該類 XMLScriptBuilder 的一個(gè)匿名內(nèi)部類仰美,而匿名內(nèi)部類的功能也很簡單赴叹,就是解析處理對應(yīng)類型的標(biāo)簽節(jié)點(diǎn)弓千,在后續(xù)應(yīng)用程序使用動(dòng)態(tài)標(biāo)簽的時(shí)候齿诞,Mybatis 隨時(shí)到 Map 池子中匹配對應(yīng)的標(biāo)簽節(jié)點(diǎn)處理器酸休,然后進(jìn)解析即可。下面我們分別對這 9 大動(dòng)態(tài) SQL 標(biāo)簽進(jìn)行介紹祷杈,排(gen)名(ju)不(wo)分(de)先(xi)后(hao):


Top1斑司、if 標(biāo)簽

常用度:★★★★★

實(shí)用性:★★★★☆

if 標(biāo)簽,絕對算得上是一個(gè)偉大的標(biāo)簽,任何不支持流程控制(或語句控制)的應(yīng)用程序宿刮,都是耍流氓互站,幾乎都不具備現(xiàn)實(shí)意義,實(shí)際的應(yīng)用場景和流程必然存在條件的控制與流轉(zhuǎn)僵缺,而 if 標(biāo)簽在 單條件分支判斷 應(yīng)用場景中就起到了舍我其誰的作用胡桃,語法很簡單,如果滿足磕潮,則執(zhí)行翠胰,不滿足,則忽略/跳過自脯。

  • if 標(biāo)簽 : 內(nèi)嵌于 select / delete / update / insert 標(biāo)簽之景,如果滿足 test 屬性的條件,則執(zhí)行代碼塊
  • test 屬性 :作為 if 標(biāo)簽的屬性膏潮,用于條件判斷锻狗,使用 OGNL 表達(dá)式。

舉個(gè)例子:

<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 標(biāo)簽元素常用于包含 where 子句的條件拼接轻纪,它相當(dāng)于 Java 中的 if 語句,和 test 屬性搭配使用叠纷,通過判斷參數(shù)值來決定是否使用某個(gè)查詢條件桐磁,也可用于 Update 語句中判斷是否更新某個(gè)字段,或用于 Insert 語句中判斷是否插入某個(gè)字段的值讲岁。

每一個(gè) if 標(biāo)簽在進(jìn)行單條件判斷時(shí)我擂,需要把判斷條件設(shè)置在 test 屬性中,這是一個(gè)常見的應(yīng)用場景缓艳,我們常用的用戶查詢系統(tǒng)功能中校摩,在前端一般提供很多可選的查詢項(xiàng),支持性別篩選阶淘、年齡區(qū)間篩查衙吩、姓名模糊匹配等,那么我們程序中接收用戶輸入之后溪窒,Mybatis 的動(dòng)態(tài) SQL 節(jié)省我們很多工作坤塞,允許我們在代碼層面不進(jìn)行參數(shù)邏輯處理和 SQL 拼接,而是把參數(shù)傳入到 SQL 中進(jìn)行條件判斷動(dòng)態(tài)處理澈蚌,我們只需要把精力集中在 XML 的維護(hù)上摹芙,既靈活也方便維護(hù),可讀性還強(qiáng)宛瞄。

image

有些心細(xì)的朋友可能就發(fā)現(xiàn)一個(gè)問題浮禾,為什么 where 語句會添加一個(gè) 1=1 呢?其實(shí)我們是為了方便拼接后面符合條件的 if 標(biāo)簽語句塊,否則沒有 1=1 的話我們拼接的 SQL 就會變成 select * from user where and age > 0 , 顯然這不是我們期望的結(jié)果盈电,當(dāng)然也不符合 SQL 的語法蝴簇,數(shù)據(jù)庫也不可能執(zhí)行成功,所以我們投機(jī)取巧添加了 1=1 這個(gè)語句匆帚,但是始終覺得多余且沒必要熬词,Mybatis 也考慮到了,所以等會我們講 where 標(biāo)簽吸重,它是如何完美解決這個(gè)問題的荡澎。

注意:if 標(biāo)簽作為單條件分支判斷,只能控制與非此即彼的流程晤锹,例如以上的例子摩幔,如果年齡 age 和姓名 name 都不存在,那么系統(tǒng)會把所有結(jié)果都查詢出來鞭铆,但有些時(shí)候或衡,我們希望系統(tǒng)更加靈活,能有更多的流程分支车遂,例如像我們 Java 當(dāng)中的 if else 或 switch case default封断,不僅僅只有一個(gè)條件分支,所以接下來我們介紹 choose 標(biāo)簽舶担,它就能滿足多分支判斷的應(yīng)用場景坡疼。


Top2、choose 標(biāo)簽衣陶、when 標(biāo)簽柄瑰、otherwise 標(biāo)簽

常用度:★★★★☆

實(shí)用性:★★★★☆

有些時(shí)候,我們并不希望條件控制是非此即彼的剪况,而是希望能提供多個(gè)條件并從中選擇一個(gè)教沾,所以貼心的 Mybatis 提供了 choose 標(biāo)簽元素,類似我們 Java 當(dāng)中的 if else 或 switch case default译断,choose 標(biāo)簽必須搭配 when 標(biāo)簽和 otherwise 標(biāo)簽使用授翻,驗(yàn)證條件依然是使用 test 屬性進(jìn)行驗(yàn)證。

  • choose 標(biāo)簽:頂層的多分支標(biāo)簽孙咪,單獨(dú)使用無意義
  • when 標(biāo)簽:內(nèi)嵌于 choose 標(biāo)簽之中堪唐,當(dāng)滿足某個(gè) when 條件時(shí),執(zhí)行對應(yīng)的代碼塊翎蹈,并終止跳出 choose 標(biāo)簽淮菠,choose 中必須至少存在一個(gè) when 標(biāo)簽,否則無意義
  • otherwise 標(biāo)簽:內(nèi)嵌于 choose 標(biāo)簽之中杨蛋,當(dāng)不滿足所有 when 條件時(shí)兜材,則執(zhí)行 otherwise 代碼塊,choose 中 至多 存在一個(gè) otherwise 標(biāo)簽逞力,可以不存在該標(biāo)簽
  • test 屬性 :作為 when 與 otherwise 標(biāo)簽的屬性曙寡,作為條件判斷,使用 OGNL 表達(dá)式

依據(jù)下面的例子寇荧,當(dāng)應(yīng)用程序輸入年齡 age 或者姓名 name 時(shí)举庶,會執(zhí)行對應(yīng)的 when 標(biāo)簽內(nèi)的代碼塊,如果 when 標(biāo)簽的年齡 age 和姓名 name 都不滿足揩抡,則會拼接 otherwise 標(biāo)簽內(nèi)的代碼塊户侥。

<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>
image

很明顯,choose 標(biāo)簽作為多分支條件判斷峦嗤,提供了更多靈活的流程控制蕊唐,同時(shí) otherwise 的出現(xiàn)也為程序流程控制兜底,有時(shí)能夠避免部分系統(tǒng)風(fēng)險(xiǎn)烁设、過濾部分條件替梨、避免當(dāng)程序沒有匹配到條件時(shí),把整個(gè)數(shù)據(jù)庫資源全部查詢或更新装黑。

至于為何 choose 標(biāo)簽這么棒棒副瀑,而常用度還是比 if 標(biāo)簽少了一顆星呢?原因也簡單恋谭,因?yàn)?choose 標(biāo)簽的很多使用場景可以直接用 if 標(biāo)簽代替糠睡。另外據(jù)我統(tǒng)計(jì),if 標(biāo)簽在實(shí)際業(yè)務(wù)應(yīng)用當(dāng)中疚颊,也要多于 choose 標(biāo)簽狈孔,大家也可以具體核查自己的應(yīng)用程序中動(dòng)態(tài) SQL 標(biāo)簽的占比情況,統(tǒng)計(jì)分析一下材义。


Top3除抛、foreach 標(biāo)簽

常用度:★★★☆☆

實(shí)用性:★★★★☆

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

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

有些場景到忽,可能需要更新 500 個(gè)用戶的姓名

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

請問大家

很多增刪改查場景清寇,操作對象都是集合/列表

如果是你來設(shè)計(jì)支持 Mybatis 的這一類集合/列表遍歷場景喘漏,你會提供什么能力的標(biāo)簽來輔助構(gòu)建你的 SQL 語句從而去滿足此類業(yè)務(wù)場景呢?

image

額(⊙o⊙)…

那如果一定要用 Mybatis 框架呢华烟?

image

沒錯(cuò)翩迈,確實(shí) Mybatis 提供了 foreach 標(biāo)簽來處理這幾類需要遍歷集合的場景,foreach 標(biāo)簽作為一個(gè)循環(huán)語句盔夜,他能夠很好的支持?jǐn)?shù)組负饲、Map堤魁、或?qū)崿F(xiàn)了 Iterable 接口(List、Set)等返十,尤其是在構(gòu)建 in 條件語句的時(shí)候妥泉,我們常規(guī)的用法都是 id in (1,2,3,4,5 ... 100) ,理論上我們可以在程序代碼中拼接字符串然后通過 ${ ids } 方式來傳值獲取洞坑,但是這種方式不能防止 SQL 注入風(fēng)險(xiǎn)盲链,同時(shí)也特別容易拼接錯(cuò)誤,所以我們此時(shí)就需要使用 #{} + foreach 標(biāo)簽來配合使用迟杂,以滿足我們實(shí)際的業(yè)務(wù)需求刽沾。譬如我們傳入一個(gè) 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);

當(dāng)然你也可以這樣編寫:

<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;

在數(shù)據(jù)量大的情況下這個(gè)性能會比較尷尬,這里僅僅做一個(gè)用法的舉例牛欢。所以經(jīng)過上面的舉栗悍抑,相信大家也基本能猜出 foreach 標(biāo)簽元素的基本用法:

  • foreach 標(biāo)簽:頂層的遍歷標(biāo)簽劫恒,單獨(dú)使用無意義
  • collection 屬性:必填,Map 或者數(shù)組或者列表的屬性名(不同類型的值獲取下面會講解)
  • item 屬性:變量名令境,值為遍歷的每一個(gè)值(可以是對象或基礎(chǔ)類型)杠园,如果是對象那么依舊是 OGNL 表達(dá)式取值即可,例如 #{item.id} 舔庶、#{ user.name } 等
  • index 屬性:索引的屬性名抛蚁,在遍歷列表或數(shù)組時(shí)為當(dāng)前索引值,當(dāng)?shù)膶ο髸r(shí) Map 類型時(shí)惕橙,該值為 Map 的鍵值(key)
  • open 屬性:循環(huán)內(nèi)容開頭拼接的字符串瞧甩,可以是空字符串
  • close 屬性:循環(huán)內(nèi)容結(jié)尾拼接的字符串,可以是空字符串
  • separator 屬性:每次循環(huán)的分隔符

第一弥鹦,當(dāng)傳入的參數(shù)為 List 對象時(shí)肚逸,系統(tǒng)會默認(rèn)添加一個(gè) key 為 'list' 的值,把列表內(nèi)容放到這個(gè) key 為 list 的集合當(dāng)中彬坏,在 foreach 標(biāo)簽中可以直接通過 collection="list" 獲取到 List 對象吼虎,無論你傳入時(shí)使用 kkk 或者 aaa ,都無所謂,系統(tǒng)都會默認(rèn)添加一個(gè) 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>

第二混滔,當(dāng)傳入的參數(shù)為數(shù)組時(shí)洒疚,系統(tǒng)會默認(rèn)添加一個(gè) key 為 'array' 的值歹颓,把列表內(nèi)容放到這個(gè) key 為 array 的集合當(dāng)中,在 foreach 標(biāo)簽中可以直接通過 collection="array" 獲取到數(shù)組對象油湖,無論你傳入時(shí)使用 ids 或者 aaa ,都無所謂巍扛,系統(tǒng)都會默認(rèn)添加一個(gè) 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>

第三,當(dāng)傳入的參數(shù)為 Map 對象時(shí)喊括,系統(tǒng)并 不會 默認(rèn)添加一個(gè) key 值胧瓜,需要手工傳入,例如傳入 key 值為 map2 的集合對象郑什,在 foreach 標(biāo)簽中可以直接通過 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>
image

可能你會覺得 Map 受到不公平對待碎捺,為何 map 不能像 List 或者 Array 一樣,在框架默認(rèn)設(shè)置一個(gè) 'map' 的 key 值呢玄窝?但其實(shí)不是不公平牵寺,而是我們在 Mybatis 框架中,所有傳入的任何參數(shù)都會供上下文使用恩脂,于是參數(shù)會被統(tǒng)一放到一個(gè)內(nèi)置參數(shù)池子里面帽氓,這個(gè)內(nèi)置參數(shù)池子的數(shù)據(jù)結(jié)構(gòu)是一個(gè) map 集合,而這個(gè) map 集合可以通過使用 “_parameter” 來獲取俩块,所有 key 都會存儲在 _parameter 集合中黎休,因此:

  • 當(dāng)你傳入的參數(shù)是一個(gè) list 類型時(shí),那么這個(gè)參數(shù)池子需要有一個(gè) key 值玉凯,以供上下文獲取這個(gè) list 類型的對象势腮,所以默認(rèn)設(shè)置了一個(gè) 'list' 字符串作為 key 值,獲取時(shí)通過使用 _parameter.list 來獲取漫仆,一般使用 list 即可捎拯。
  • 同樣的,當(dāng)你傳入的參數(shù)是一個(gè) array 數(shù)組時(shí)盲厌,那么這個(gè)參數(shù)池子也會默認(rèn)設(shè)置了一個(gè) 'array' 字符串作為 key 值署照,以供上下文獲取這個(gè) array 數(shù)組的對象值祸泪,獲取時(shí)通過使用 _parameter.array 來獲取,一般使用 array 即可建芙。
  • 但是没隘!當(dāng)你傳入的參數(shù)是一個(gè) map 集合類型時(shí),那么這個(gè)參數(shù)池就沒必要為你添加默認(rèn) key 值了禁荸,因?yàn)?map 集合類型本身就會有很多 key 值右蒲,例如你想獲取 map 參數(shù)的某個(gè) key 值,你可以直接使用 _parameter.name 或者 _parameter.age 即可赶熟,就沒必要還用 _parameter.map.name 或者 _parameter.map.age 瑰妄,所以這就是 map 參數(shù)類型無需再構(gòu)建一個(gè) 'map' 字符串作為 key 的原因,對象類型也是如此钧大,例如你傳入一個(gè) 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",你會發(fā)現(xiàn)神奇的 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>
image

延伸:當(dāng)傳入?yún)?shù)為多個(gè)對象時(shí)瓜饥,例如傳入 User 和 Room 等,那么通過內(nèi)置參數(shù)獲取對象可以使用 _parameter.get(0).username浴骂,或者 _parameter.get(1).roomname 乓土。假如你傳入的參數(shù)是一個(gè)簡單數(shù)據(jù)類型,例如傳入 int =1 或者 String = '你好'溯警,那么都可以直接使用 _parameter 代替獲取值即可趣苏,這就是很多人會在動(dòng)態(tài) SQL 中直接使用 # { _parameter } 來獲取簡單數(shù)據(jù)類型的值。

那到這里梯轻,我們基本把 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 個(gè)用戶的姓名:

<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>

更多玩法,期待你自己去挖掘曹宴!

image

注意:使用 foreach 標(biāo)簽時(shí)搂橙,需要對傳入的 collection 參數(shù)(List/Map/Set等)進(jìn)行為空性判斷,否則動(dòng)態(tài) SQL 會出現(xiàn)語法異常笛坦,例如你的查詢語句可能是 select * from user where ids in () 区转,導(dǎo)致以上語法異常就是傳入?yún)?shù)為空唯袄,解決方案可以用 if 標(biāo)簽或 choose 標(biāo)簽進(jìn)行為空性判斷處理,或者直接在 Java 代碼中進(jìn)行邏輯處理即可蜗帜,例如判斷為空則不執(zhí)行 SQL 恋拷。


Top4、where 標(biāo)簽厅缺、set 標(biāo)簽

常用度:★★☆☆☆

實(shí)用性:★★★★☆

我們把 where 標(biāo)簽和 set 標(biāo)簽放置一起講解蔬顾,一是這兩個(gè)標(biāo)簽在實(shí)際應(yīng)用開發(fā)中常用度確實(shí)不分伯仲,二是這兩個(gè)標(biāo)簽出自一家湘捎,都繼承了 trim 標(biāo)簽诀豁,放置一起方便我們比對追根。(其中底層原理會在第4部分詳細(xì)講解)

image

之前我們介紹 if 標(biāo)簽的時(shí)候窥妇,相信大家都已經(jīng)看到舷胜,我們在 where 子句后面拼接了 1=1 的條件語句塊,目的是為了保證后續(xù)條件能夠正確拼接活翩,以前在程序代碼中使用字符串拼接 SQL 條件語句常常如此使用烹骨,但是確實(shí)此種方式不夠體面,也顯得我們不高級材泄。

<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 標(biāo)簽誕生之后眯搭,是怎么巧妙處理后續(xù)的條件語句的呢瘸羡?

<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 關(guān)鍵詞以及 1=1 改為 < where > 標(biāo)簽即可,另外還有一個(gè)特殊的處理能力戚扳,就是 where 標(biāo)簽?zāi)軌蛑悄艿娜コê雎裕┦讉€(gè)滿足條件語句的前綴旦事,例如以上條件如果 age 和 name 都滿足魁巩,那么 age 前綴 and 會被智能去除掉,無論你是使用 and 運(yùn)算符或是 or 運(yùn)算符姐浮,Mybatis 框架都會幫你智能處理谷遂。

用法特別簡單,我們用官術(shù)總結(jié)一下

  • where 標(biāo)簽:頂層的遍歷標(biāo)簽单料,需要配合 if 標(biāo)簽使用埋凯,單獨(dú)使用無意義,并且只會在子元素(如 if 標(biāo)簽)返回任何內(nèi)容的情況下才插入 WHERE 子句扫尖。另外白对,若子句的開頭為 “AND” 或 “OR”,where 標(biāo)簽也會將它替換去除换怖。
image

了解了基本用法之后甩恼,我們再看看剛剛我們的例子中:

<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 標(biāo)簽并去除首個(gè)子句運(yùn)算符 and条摸,最終的 SQL 語句會變成:

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

值得注意的是悦污,where 標(biāo)簽 只會 智能的去除(忽略)首個(gè)滿足條件語句的前綴,所以就建議我們在使用 where 標(biāo)簽的時(shí)候钉蒲,每個(gè)語句都最好寫上 and 前綴或者 or 前綴切端,否則像以下寫法就很有可能出大事:

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

當(dāng) age 傳入 10,name 傳入 ‘潘潘’ 時(shí)顷啼,最終的 SQL 語句是:

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

由于 name 前綴沒有寫 and 或 or 連接符踏枣,而 where 標(biāo)簽又不會智能的去除(忽略)非首個(gè) 滿足條件語句的前綴,所以當(dāng) age 條件語句與 name 條件語句同時(shí)成立時(shí)钙蒙,就會導(dǎo)致語法錯(cuò)誤茵瀑,這個(gè)需要謹(jǐn)慎使用,格外注意躬厌!原則上每個(gè)條件子句都建議在句首添加運(yùn)算符 and 或 or ,首個(gè)條件語句可添加可不加马昨。

另外還有一個(gè)值得注意的點(diǎn),我們使用 XML 方式配置 SQL 時(shí)扛施,如果在 where 標(biāo)簽之后添加了注釋鸿捧,那么當(dāng)有子元素滿足條件時(shí),除了 < !-- --> 注釋會被 where 忽略解析以外煮嫌,其它注釋例如 // 或 /**/ 或 -- 等都會被 where 當(dāng)成首個(gè)子句元素處理笛谦,導(dǎo)致后續(xù)真正的首個(gè) AND 子句元素或 OR 子句元素沒能被成功替換掉前綴抱虐,從而引起語法錯(cuò)誤昌阿!

image

基于 where 標(biāo)簽元素的講解,有助于我們快速理解 set 標(biāo)簽元素恳邀,畢竟它倆是如此相像懦冰。我們回憶一下以往我們的更新 SQL 語句:

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

以上語句是我們?nèi)粘S糜诟轮付?id 對象的 age 字段、 username 字段以及 password 字段谣沸,但是很多時(shí)候刷钢,我們可能只希望更新對象的某些字段,而不是每次都更新對象的所有字段乳附,這就使得我們在語句結(jié)構(gòu)的構(gòu)建上顯得慘白無力内地。于是有了 set 標(biāo)簽元素。

用法與 where 標(biāo)簽元素相似

  • set 標(biāo)簽:頂層的遍歷標(biāo)簽赋除,需要配合 if 標(biāo)簽使用阱缓,單獨(dú)使用無意義,并且只會在子元素(如 if 標(biāo)簽)返回任何內(nèi)容的情況下才插入 set 子句举农。另外荆针,若子句的 開頭或結(jié)尾 都存在逗號 “,” 則 set 標(biāo)簽都會將它替換去除。
image

根據(jù)此用法我們可以把以上的例子改為:

<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 標(biāo)簽會智能拼接更新字段航背,以上例子如果傳入 age =10 和 username = '潘潘' 喉悴,則有兩個(gè)字段滿足更新條件,于是 set 標(biāo)簽會智能拼接 " age = 10 ," 和 "username = '潘潘' ," 玖媚。其中由于后一個(gè) username 屬于最后一個(gè)子句箕肃,所以末尾逗號會被智能去除,最終的 SQL 語句是:

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

另外需要注意今魔,set 標(biāo)簽下需要保證至少有一個(gè)條件滿足突雪,否則依然會產(chǎn)生語法錯(cuò)誤,例如在無子句條件滿足的場景下涡贱,最終的 SQL 語句會是這樣:

update user ;  ( oh~ no!)

既不會添加 set 標(biāo)簽咏删,也沒有子句更新字段,于是語法出現(xiàn)了錯(cuò)誤问词,所以類似這類情況督函,一般需要在應(yīng)用程序中進(jìn)行邏輯處理,判斷是否存在至少一個(gè)參數(shù)激挪,否則不執(zhí)行更新 SQL 辰狡。所以原則上要求 set 標(biāo)簽下至少存在一個(gè)條件滿足,同時(shí)每個(gè)條件子句都建議在句末添加逗號 ,最后一個(gè)條件語句可加可不加垄分。或者 每個(gè)條件子句都在句首添加逗號 ,第一個(gè)條件語句可加可不加宛篇,例如:

<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 標(biāo)簽相同,我們使用 XML 方式配置 SQL 時(shí)薄湿,如果在 set 標(biāo)簽子句末尾添加了注釋叫倍,那么當(dāng)有子元素滿足條件時(shí),除了 < !-- --> 注釋會被 set 忽略解析以外豺瘤,其它注釋例如 // 或 /**/ 或 -- 等都會被 set 標(biāo)簽當(dāng)成末尾子句元素處理吆倦,導(dǎo)致后續(xù)真正的末尾子句元素的逗號沒能被成功替換掉后綴,從而引起語法錯(cuò)誤坐求!

image

到此蚕泽,我們的 where 標(biāo)簽元素與 set 標(biāo)簽就基本介紹完成,它倆確實(shí)極為相似桥嗤,區(qū)別僅在于:

  • where 標(biāo)簽插入前綴 where
  • set 標(biāo)簽插入前綴 set
  • where 標(biāo)簽僅智能替換前綴 AND 或 OR
  • set 標(biāo)簽可以只能替換前綴逗號须妻,或后綴逗號,

而這兩者的前后綴去除策略泛领,都源自于 trim 標(biāo)簽的設(shè)計(jì)荒吏,我們一起看看到底 trim 標(biāo)簽是有多靈活!


Top5师逸、trim 標(biāo)簽

常用度:★☆☆☆☆

實(shí)用性:★☆☆☆☆

上面我們介紹了 where 標(biāo)簽與 set 標(biāo)簽司倚,它倆的共同點(diǎn)無非就是前置關(guān)鍵詞 where 或 set 的插入豆混,以及前后綴符號(例如 AND | OR | ,)的智能去除动知∶笏牛基于 where 標(biāo)簽和 set 標(biāo)簽本身都繼承了 trim 標(biāo)簽,所以 trim 標(biāo)簽的大致實(shí)現(xiàn)我們也能猜出個(gè)一二三盒粮。

image

其實(shí) where 標(biāo)簽和 set 標(biāo)簽都只是 trim 標(biāo)簽的某種實(shí)現(xiàn)方案鸵鸥,trim 標(biāo)簽底層是通過 TrimSqlNode 類來實(shí)現(xiàn)的,它有幾個(gè)關(guān)鍵屬性:

  • prefix :前綴丹皱,當(dāng) trim 元素內(nèi)存在內(nèi)容時(shí)妒穴,會給內(nèi)容插入指定前綴
  • suffix :后綴,當(dāng) trim 元素內(nèi)存在內(nèi)容時(shí)摊崭,會給內(nèi)容插入指定后綴
  • prefixesToOverride :前綴去除讼油,支持多個(gè),當(dāng) trim 元素內(nèi)存在內(nèi)容時(shí)呢簸,會把內(nèi)容中匹配的前綴字符串去除矮台。
  • suffixesToOverride :后綴去除,支持多個(gè)根时,當(dāng) trim 元素內(nèi)存在內(nèi)容時(shí)瘦赫,會把內(nèi)容中匹配的后綴字符串去除。

所以 where 標(biāo)簽如果通過 trim 標(biāo)簽實(shí)現(xiàn)的話可以這么編寫:(

<!--
  注意在使用 trim 標(biāo)簽實(shí)現(xiàn) where 標(biāo)簽?zāi)芰r(shí)
  必須在 AND 和 OR 之后添加空格
  避免匹配到 android蛤迎、order 等單詞 
-->
<trim prefix="WHERE" prefixOverrides="AND | OR" >
    ...
</trim>

而 set 標(biāo)簽如果通過 trim 標(biāo)簽實(shí)現(xiàn)的話可以這么編寫:

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

或者

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

所以可見 trim 是足夠靈活的确虱,不過由于 where 標(biāo)簽和 set 標(biāo)簽這兩種 trim 標(biāo)簽變種方案已經(jīng)足以滿足我們實(shí)際開發(fā)需求,所以直接使用 trim 標(biāo)簽的場景實(shí)際上不太很多(其實(shí)是我自己使用的不多替裆,基本沒用過)校辩。

注意,set 標(biāo)簽之所以能夠支持去除前綴逗號或者后綴逗號扎唾,是由于其在構(gòu)造 trim 標(biāo)簽的時(shí)候進(jìn)行了前綴后綴的去除設(shè)置召川,而 where 標(biāo)簽在構(gòu)造 trim 標(biāo)簽的時(shí)候就僅僅設(shè)置了前綴去除。

set 標(biāo)簽元素之構(gòu)造時(shí):

// Set 標(biāo)簽
public class SetSqlNode extends TrimSqlNode {

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

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

}

where 標(biāo)簽元素之構(gòu)造時(shí):

// Where 標(biāo)簽
public class WhereSqlNode extends TrimSqlNode {

  // 其實(shí)包含了很多種場景
  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 標(biāo)簽

常用度:☆☆☆☆☆

實(shí)用性:★☆☆☆☆

簡單來說,這個(gè)標(biāo)簽就是可以創(chuàng)建一個(gè)變量概疆,并綁定到上下文逗威,即供上下文使用,就是這樣岔冀,我把官網(wǎng)的例子直接拷貝過來:

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

大家應(yīng)該大致能知道以上例子的功效凯旭,其實(shí)就是輔助構(gòu)建模糊查詢的語句拼接,那有人就好奇了,為啥不直接拼接語句就行了罐呼,為什么還要搞出一個(gè)變量鞠柄,繞一圈呢?

image

我先問一個(gè)問題:平時(shí)你使用 mysql 都是如何拼接模糊查詢 like 語句的嫉柴?

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

確實(shí)如此厌杜,但如果有一天領(lǐng)導(dǎo)跟你說數(shù)據(jù)庫換成 oracle 了,怎么辦计螺?上面的語句還能用嗎夯尽?明顯用不了,不能這么寫登馒,因?yàn)?oracle 雖然也有 concat 函數(shù)匙握,但是只支持連接兩個(gè)字符串,例如你最多這么寫:

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

但是少了右邊的井號符號陈轿,所以達(dá)不到你預(yù)期的效果肺孤,于是你改成這樣:

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

確實(shí)可以了,但是過幾天領(lǐng)導(dǎo)又跟你說济欢,數(shù)據(jù)庫換回 mysql 了赠堵?額… 那不好意思,你又得把相關(guān)使用到模糊查詢的地方改回來法褥。

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

很顯然茫叭,數(shù)據(jù)庫只要發(fā)生變更你的 sql 語句就得跟著改,特別麻煩半等,所以才有了一開始我們介紹 bind 標(biāo)簽官網(wǎng)的這個(gè)例子揍愁,無論使用哪種數(shù)據(jù)庫,這個(gè)模糊查詢的 Like 語法都是支持的:

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

這個(gè) bind 的用法杀饵,實(shí)打?qū)嵔鉀Q了數(shù)據(jù)庫重新選型后導(dǎo)致的一些問題莽囤,當(dāng)然在實(shí)際工作中發(fā)生的概率不會太大切距,所以 bind 的使用我個(gè)人確實(shí)也使用的不多床蜘,可能還有其它一些應(yīng)用場景辙培,希望有人能發(fā)現(xiàn)之后來跟我們分享一下,總之我勉強(qiáng)給了一顆星(雖然沒太多實(shí)際用處邢锯,但畢竟要給點(diǎn)面子)扬蕊。


拓展:sql標(biāo)簽 + include 標(biāo)簽

常用度:★★★☆☆

實(shí)用性:★★★☆☆

sql 標(biāo)簽與 include 標(biāo)簽組合使用,用于 SQL 語句的復(fù)用弹囚,日常高頻或公用使用的語句塊可以抽取出來進(jìn)行復(fù)用厨相,其實(shí)我們應(yīng)該不陌生,早期我們學(xué)習(xí) JSP 的時(shí)候鸥鹉,就有一個(gè) include 標(biāo)記可以引入一些公用可復(fù)用的頁面文件蛮穿,例如頁面頭部或尾部頁面代碼元素,這種復(fù)用的設(shè)計(jì)很常見毁渗。

嚴(yán)格意義上 sql 践磅、include 不算在動(dòng)態(tài) SQL 標(biāo)簽成員之內(nèi),只因它確實(shí)是寶藏般的存在灸异,所以我要簡單說說府适,sql 標(biāo)簽用于定義一段可重用的 SQL 語句片段,以便在其它語句中使用肺樟,而 include 標(biāo)簽則通過屬性 refid 來引用對應(yīng) id 匹配的 sql 標(biāo)簽語句片段檐春。

image

簡單的復(fù)用代碼塊可以是:

<!-- 可復(fù)用的字段語句塊 -->
<sql id="userColumns">
    id,username,password 
</sql>

查詢或插入時(shí)簡單復(fù)用:

<!-- 查詢時(shí)簡單復(fù)用 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"></include> 
  from user 
</select>

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

當(dāng)然,復(fù)用語句還支持屬性傳遞么伯,例如:

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

這個(gè) SQL 片段可以在其它語句中使用:

<!-- 查詢時(shí)復(fù)用 -->
<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 屬性或多層內(nèi)部語句中使用屬性值疟暖,屬性可以穿透傳遞,例如:

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

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

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

至此田柔,關(guān)于 9 大動(dòng)態(tài) SQL 標(biāo)簽的基本用法我們已介紹完畢俐巴,另外我們還有一些疑問:Mybatis 底層是如何解析這些動(dòng)態(tài) SQL 標(biāo)簽的呢?最終又是怎么構(gòu)建完整可執(zhí)行的 SQL 語句的呢硬爆?帶著這些疑問欣舵,我們在第4節(jié)中詳細(xì)分析。

image
image

4缀磕、動(dòng)態(tài)SQL的底層原理

想了解 Mybatis 究竟是如何解析與構(gòu)建動(dòng)態(tài) SQL 缘圈?首先推薦的當(dāng)然是讀源碼,而讀源碼虐骑,是一個(gè)技術(shù)鉆研問題准验,為了借鑒學(xué)習(xí),為了工作儲備廷没,為了解決問題,為了讓自己在編程的道路上跑得明白一些... 而希望通過讀源碼垂寥,去了解底層實(shí)現(xiàn)原理颠黎,切記不能脫離了整體去讀局部另锋,否則你了解到的必然局限且片面,從而輕忽了真核上的設(shè)計(jì)狭归。如同我們讀史或者觀宇宙一樣夭坪,最好的辦法都是從整體到局部,不斷放大过椎,前后延展室梅,會很舒服通透。所以我準(zhǔn)備從 Mybatis 框架的核心主線上去逐步放大剖析疚宇。

image

通過前面幾篇文章的介紹(建議閱讀 Mybatis 系列全解之六:《Mybatis 最硬核的 API 你知道幾個(gè)亡鼠?》),其實(shí)我們知道了 Mybatis 框架的核心部分在于構(gòu)件的構(gòu)建過程敷待,從而支撐了外部應(yīng)用程序的使用间涵,從應(yīng)用程序端創(chuàng)建配置并調(diào)用 API 開始,到框架端加載配置并初始化構(gòu)件榜揖,再創(chuàng)建會話并接收請求勾哩,然后處理請求,最終返回處理結(jié)果等举哟。

image

我們的動(dòng)態(tài) SQL 解析部分就發(fā)生在 SQL 語句對象 MappedStatement 構(gòu)建時(shí)(上左高亮橘色部分思劳,注意觀察其中 SQL 語句對象與 SqlSource 、 BoundSql 的關(guān)系妨猩,在動(dòng)態(tài) SQL 解析流程特別關(guān)鍵)潜叛。我們再拉近一點(diǎn),可以看到無論是使用 XML 配置 SQL 語句或是使用注解方式配置 SQL 語句册赛,框架最終都會把解析完成的 SQL 語句對象存放到 MappedStatement 語句集合池子钠导。

image

而以上虛線高亮部分,即是 XML 配置方式解析過程與注解配置方式解析過程中涉及到動(dòng)態(tài) SQL 標(biāo)簽解析的流程森瘪,我們分別講解:

  • 第一牡属,XML 方式配置 SQL 語句,框架如何解析扼睬?
image

以上為 XML 配置方式的 SQL 語句解析過程逮栅,無論是單獨(dú)使用 Mybatis 框架還是集成 Spring 與 Mybatis 框架,程序啟動(dòng)入口都會首先從 SqlSessionFactoryBuilder.build() 開始構(gòu)建窗宇,依次通過 XMLConfigBuilder 構(gòu)建全局配置 Configuration 對象措伐、通過 XMLMapperBuilder 構(gòu)建每一個(gè) Mapper 映射器、通過 XMLStatementBuilder 構(gòu)建映射器中的每一個(gè) SQL 語句對象(select/insert/update/delete)军俊。而就在解析構(gòu)建每一個(gè) SQL 語句對象時(shí)侥加,涉及到一個(gè)關(guān)鍵的方法 parseStatementNode(),即上圖橘紅色高亮部分粪躬,此方法內(nèi)部就出現(xiàn)了一個(gè)處理動(dòng)態(tài) SQL 的核心節(jié)點(diǎn)担败。

// XML配置語句構(gòu)建器
public class XMLStatementBuilder {
    
    // 實(shí)際解析每一個(gè) SQL 語句
    // 例如 select|insert|update|delete
    public void parseStatementNode() {
        
        // [忽略]參數(shù)構(gòu)建...
        // [忽略]緩存構(gòu)建..
        // [忽略]結(jié)果集構(gòu)建等等.. 
        
        // 【重點(diǎn)】此處即是處理動(dòng)態(tài) SQL 的核心N粞ā!提前!
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
        SqlSource sqlSource = langDriver.createSqlSource(..);
        
        // [忽略]最后把解析完成的語句對象添加進(jìn)語句集合池
        builderAssistant.addMappedStatement(語句對象)
    }
}

大家先重點(diǎn)關(guān)注一下這段代碼吗货,其中【重點(diǎn)】部分的 LanguageDriver 與 SqlSource 會是我們接下來講解動(dòng)態(tài) SQL 語句解析的核心類,我們不著急剖析狈网,我們先把注解方式流程也梳理對比一下宙搬。

  • 第二,注解方式配置 SQL 語句拓哺,框架如何解析勇垛?
image

大家會發(fā)現(xiàn)注解配置方式的 SQL 語句解析過程,與 XML 方式極為相像拓售,唯一不同點(diǎn)就在于解析注解 SQL 語句時(shí)窥摄,使用了 MapperAnnotationBuilder 構(gòu)建器,其中關(guān)于每一個(gè)語句對象 (@Select,@Insert,@Update,@Delete等) 的解析础淤,又都會通過一個(gè)關(guān)鍵解析方法 parseStatement()崭放,即上圖橘紅色高亮部分,此方法內(nèi)部同樣的出現(xiàn)了一個(gè)處理動(dòng)態(tài) SQL 的核心節(jié)點(diǎn)鸽凶。

// 注解配置語句構(gòu)建器
public class MapperAnnotationBuilder {
    
    // 實(shí)際解析每一個(gè) SQL 語句
    // 例如 @Select,@Insert,@Update,@Delete
    void parseStatement(Method method) {  
        
        // [忽略]參數(shù)構(gòu)建...
        // [忽略]緩存構(gòu)建..
        // [忽略]結(jié)果集構(gòu)建等等.. 
        
        // 【重點(diǎn)】此處即是處理動(dòng)態(tài) SQL 的核心1疑啊!玻侥!
        final LanguageDriver languageDriver = getLanguageDriver(method);  
        final SqlSource sqlSource = buildSqlSource( languageDriver,... );
        
        // [忽略]最后把解析完成的語句對象添加進(jìn)語句集合池
        builderAssistant.addMappedStatement(語句對象)

    }    
}

由此可見决摧,不管是通過 XML 配置語句還是注解方式配置語句,構(gòu)建流程都是 大致相同凑兰,并且依然出現(xiàn)了我們在 XML 配置方式中涉及到的語言驅(qū)動(dòng) LanguageDriver 與語句源 SqlSource 掌桩,那這兩個(gè)類/接口到底為何物,為何能讓 SQL 語句解析者都如此繞不開 姑食?

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

image

我們知道,無論 XML 還是注解音半,最終你的所有 SQL 語句對象都會被齊齊整整的解析完放置在 SQL 語句對象集合池中则拷,以供執(zhí)行器 Executor 具體執(zhí)行增刪改查 ( CRUD ) 時(shí)使用。而我們知道每一個(gè) SQL 語句對象的屬性曹鸠,特別復(fù)雜繁多煌茬,例如超時(shí)設(shè)置、緩存彻桃、語句類型坛善、結(jié)果集映射關(guān)系等等。

// 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;
    
}

而其中有一個(gè)特別的屬性就是我們的語句源 SqlSource ,功能純粹也恰如其名 SQL 源浑吟。它是一個(gè)接口笙纤,它會結(jié)合用戶傳遞的參數(shù)對象 parameterObject 與動(dòng)態(tài) SQL耗溜,生成 SQL 語句组力,并最終封裝成 BoundSql 對象。SqlSource 接口有5個(gè)實(shí)現(xiàn)類抖拴,分別是:StaticSqlSource燎字、DynamicSqlSource、RawSqlSource阿宅、ProviderSqlSource候衍、VelocitySqlSource (而 velocitySqlSource 目前只是一個(gè)測試用例,還沒有用作實(shí)際的 Sql 源實(shí)現(xiàn))洒放。

image
  • StaticSqlSource:靜態(tài) SQL 源實(shí)現(xiàn)類蛉鹿,所有的 SQL 源最終都會構(gòu)建成 StaticSqlSource 實(shí)例,該實(shí)現(xiàn)類會生成最終可執(zhí)行的 SQL 語句供 statement 或 prepareStatement 使用往湿。
  • RawSqlSource:原生 SQL 源實(shí)現(xiàn)類妖异,解析構(gòu)建含有 ‘#{}’ 占位符的 SQL 語句或原生 SQL 語句,解析完最終會構(gòu)建 StaticSqlSource 實(shí)例领追。
  • DynamicSqlSource:動(dòng)態(tài) SQL 源實(shí)現(xiàn)類雌澄,解析構(gòu)建含有 ‘${}’ 替換符的 SQL 語句或含有動(dòng)態(tài) SQL 的語句(例如 If/Where/Foreach等)陡鹃,解析完最終會構(gòu)建 StaticSqlSource 實(shí)例。
  • ProviderSqlSource:注解方式的 SQL 源實(shí)現(xiàn)類,會根據(jù) SQL 語句的內(nèi)容分發(fā)給 RawSqlSource 或 DynamicSqlSource 痹束,當(dāng)然最終也會構(gòu)建 StaticSqlSource 實(shí)例。
  • VelocitySqlSource:模板 SQL 源實(shí)現(xiàn)類喇澡,目前(V3.5.6)官方申明這只是一個(gè)測試用例腕侄,還沒有用作真正的模板 Sql 源實(shí)現(xiàn)類。

SqlSource 實(shí)例在配置類 Configuration 解析階段就被創(chuàng)建订雾,Mybatis 框架會依據(jù)3個(gè)維度的信息來選擇構(gòu)建哪種數(shù)據(jù)源實(shí)例:(純屬我個(gè)人理解的歸類梳理~)

  • 第一個(gè)維度:客戶端的 SQL 配置方式:XML 方式或者注解方式肢预。
  • 第二個(gè)維度:SQL 語句中是否使用動(dòng)態(tài) SQL ( if/where/foreach 等 )。
  • 第三個(gè)維度:SQL 語句中是否含有替換符 ‘${}’ 或占位符 ‘#{}’ 葬燎。

SqlSource 接口只有一個(gè)方法 getBoundSql 误甚,就是創(chuàng)建 BoundSql 對象。

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

通過 SQL 源就能夠獲取 BoundSql 對象谱净,從而獲取最終送往數(shù)據(jù)庫(通過JDBC)中執(zhí)行的 SQL 字符串窑邦。

image

JDBC 中執(zhí)行的 SQL 字符串,確實(shí)就在 BoundSql 對象中壕探。BoundSql 對象存儲了動(dòng)態(tài)(或靜態(tài))生成的 SQL 語句以及相應(yīng)的參數(shù)信息冈钦,它是在執(zhí)行器具體執(zhí)行 CURD 時(shí)通過實(shí)際的 SqlSource 實(shí)例所構(gòu)建的。

public class BoundSql { 

  //該字段中記錄了SQL語句李请,該SQL語句中可能含有"?"占位符
  private final String sql;
    
  //SQL中的參數(shù)屬性集合
  private final List<ParameterMapping> parameterMappings;
    
  //客戶端執(zhí)行SQL時(shí)傳入的實(shí)際參數(shù)值
  private final Object parameterObject;
    
  //復(fù)制 DynamicContext.bindings 集合中的內(nèi)容
  private final Map<String, Object> additionalParameters;
    
  //通過 additionalParameters 構(gòu)建元參數(shù)對象
  private final MetaObject metaParameters;
    
}

在執(zhí)行器 Executor 實(shí)例(例如BaseExecutor)執(zhí)行增刪改查時(shí)瞧筛,會通過 SqlSource 構(gòu)建 BoundSql 實(shí)例厉熟,然后再通過 BoundSql 實(shí)例獲取最終輸送至數(shù)據(jù)庫執(zhí)行的 SQL 語句,系統(tǒng)可根據(jù) SQL 語句構(gòu)建 Statement 或者 PrepareStatement 较幌,從而送往數(shù)據(jù)庫執(zhí)行揍瑟,例如語句處理器 StatementHandler 的執(zhí)行過程。

image

墻裂推薦閱讀之前第六文之 Mybatis 最硬核的 API 你知道幾個(gè)乍炉?這些執(zhí)行流程都有細(xì)講绢片。

到此我們介紹完 SQL 源 SqlSource 與 BoundSql 的關(guān)系,注意 SqlSource 與 BoundSql 不是同個(gè)階段產(chǎn)生的岛琼,而是分別在程序啟動(dòng)階段與運(yùn)行時(shí)底循。

  • 程序啟動(dòng)初始構(gòu)建時(shí),框架會根據(jù) SQL 語句類型構(gòu)建對應(yīng)的 SqlSource 源實(shí)例(靜態(tài)/動(dòng)態(tài)).
  • 程序?qū)嶋H運(yùn)行時(shí)槐瑞,框架會根據(jù)傳入?yún)?shù)動(dòng)態(tài)的構(gòu)建 BoundSql 對象熙涤,輸送最終 SQL 到數(shù)據(jù)庫執(zhí)行。

在上面我們知道了 SQL 源是語句對象 BoundSql 的屬性困檩,同時(shí)還坐擁5大實(shí)現(xiàn)類祠挫,那究竟是誰創(chuàng)建了 SQL 源呢?其實(shí)就是我們接下來準(zhǔn)備介紹的語言驅(qū)動(dòng) LanguageDriver 窗看!

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

語言驅(qū)動(dòng)接口 LanguageDriver 也是極簡潔茸歧,內(nèi)部定義了構(gòu)建 SQL 源的方法,LanguageDriver 接口有2個(gè)實(shí)現(xiàn)類显沈,分別是: XMLLanguageDriver 软瞎、 RawLanguageDriver。簡單介紹一下:

image
  • XMLLanguageDriver :是框架默認(rèn)的語言驅(qū)動(dòng)拉讯,能夠根據(jù)上面我們講解的 SQL 源的3個(gè)維度創(chuàng)建對應(yīng)匹配的 SQL 源(DynamicSqlSource涤浇、RawSqlSource等)。下面這段代碼是 Mybatis 在裝配全局配置時(shí)的一些跟語言驅(qū)動(dòng)相關(guān)的動(dòng)作魔慷,我摘抄出來只锭,分別有:內(nèi)置了兩種語言驅(qū)動(dòng)并設(shè)置了別名方便引用、注冊了兩種語言驅(qū)動(dòng)至語言注冊工廠院尔、把 XML 語言驅(qū)動(dòng)設(shè)置為默認(rèn)語言驅(qū)動(dòng)蜻展。
// 全局配置的構(gòu)造方法
public Configuration() {
    // 內(nèi)置/注冊了很多有意思的【別名】
    // ...
    
    // 其中就內(nèi)置了上述的兩種語言驅(qū)動(dòng)【別名】
    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
    
    // 注冊了XML【語言驅(qū)動(dòng)】 --> 并設(shè)置成默認(rèn)!   
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    
    // 注冊了原生【語言驅(qū)動(dòng)】
    languageRegistry.register(RawLanguageDriver.class);
}
  • RawLanguageDriver :看名字得知是原生語言驅(qū)動(dòng)邀摆,事實(shí)也如此纵顾,它只能創(chuàng)建原生 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 語言驅(qū)動(dòng)就支持解析靜態(tài)語句(動(dòng)態(tài)語句當(dāng)然也支持)并創(chuàng)建對應(yīng)的 SQL 源(例如靜態(tài)語句是原生 SQL 源),所以除非你十分確定你的 SQL 語句中沒有包含任何一款動(dòng)態(tài)標(biāo)簽,否則就不要使用 RawLanguageDriver 汉额!否則會報(bào)錯(cuò)2苷獭!蠕搜!先看個(gè)別名引用的例子:

<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 屬性手工指定語言驅(qū)動(dòng)怎茫,不指定則系統(tǒng)默認(rèn)是 lang = "XML",XML 代表 XMLLanguageDriver 讥脐,當(dāng)然 lang 屬性可以是我們內(nèi)置的別名也可以是我們的語言驅(qū)動(dòng)全限定名遭居,不過值得注意的是,當(dāng)語句中含有動(dòng)態(tài) SQL 標(biāo)簽時(shí)旬渠,就只能選擇使用 lang="XML",否則程序在初始化構(gòu)件時(shí)就會報(bào)錯(cuò)端壳。

## Cause: org.apache.ibatis.builder.BuilderException: 
## Dynamic content is not allowed when using RAW language
## 動(dòng)態(tài)語句內(nèi)容不被原生語言驅(qū)動(dòng)支持告丢!

這段錯(cuò)誤提示其實(shí)是發(fā)生在 RawLanguageDriver 檢查動(dòng)態(tài) SQL 源時(shí):

public class RawLanguageDriver extends XMLLanguageDriver { 

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

至此,基本邏輯我們已經(jīng)梳理清楚:程序啟動(dòng)初始階段损谦,語言驅(qū)動(dòng)創(chuàng)建 SQL 源岖免,而運(yùn)行時(shí), SQL 源動(dòng)態(tài)解析構(gòu)建出 BoundSql 照捡。

那么除了系統(tǒng)默認(rèn)的兩種語言驅(qū)動(dòng)颅湘,還有其它嗎?

答案是:有栗精,例如 Mybatis 框架中目前使用了一個(gè)名為 VelocityLanguageDriver 的語言驅(qū)動(dòng)闯参。相信大家都學(xué)習(xí)過 JSP 模板引擎,同時(shí)還有很多人學(xué)習(xí)過其它一些(頁面)模板引擎悲立,例如 freemark 和 velocity 鹿寨,不同模板引擎有自己的一套模板語言語法,而其中 Mybatis 就嘗試使用了 Velocity 模板引擎作為語言驅(qū)動(dòng)薪夕,目前雖然 Mybatis 只是在測試用例中使用到脚草,但是它告訴了我們,框架允許自定義語言驅(qū)動(dòng)原献,所以不只是 XML馏慨、RAW 兩種語言驅(qū)動(dòng)中使用的 OGNL 語法,也可以是 Velocity (語法)姑隅,或者你自己所能定義的一套模板語言(同時(shí)你得定義一套語法)写隶。 例如以下就是 Mybatis 框架中使用到的 Velocity 語言驅(qū)動(dòng)和對應(yīng)的 SQL 源,它們使用 Velocity 語法/方式解析構(gòu)建 BoundSql 對象粤策。

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

好,語言驅(qū)動(dòng)的基本概念大致如此。我們回過頭再詳細(xì)看看動(dòng)態(tài) SQL 源 SqlSource秩贰,作為語句對象 MappedStatement 的屬性霹俺,在 程序初始構(gòu)建階段,語言驅(qū)動(dòng)是怎么創(chuàng)建它的呢毒费?不妨我們先看看常用的動(dòng)態(tài) SQL 源對象是怎么被創(chuàng)建的吧丙唧!

image

通過以上的程序初始構(gòu)建階段,我們可以發(fā)現(xiàn)觅玻,最終語言驅(qū)動(dòng)通過調(diào)用 XMLScriptBuilder 對象來創(chuàng)建 SQL 源想际。

// XML 語言驅(qū)動(dòng)
public class XMLLanguageDriver implements LanguageDriver {  
  
    // 通過調(diào)用 XMLScriptBuilder 對象來創(chuàng)建 SQL 源
    @Override
    public SqlSource createSqlSource() {
        // 實(shí)例
        XMLScriptBuilder builder = new XMLScriptBuilder();
        // 解析
        return builder.parseScriptNode();
    }
}

而在前面我們就已經(jīng)介紹, XMLScriptBuilder 實(shí)例初始構(gòu)造時(shí)溪厘,會初始構(gòu)建所有動(dòng)態(tài)標(biāo)簽處理器:

// XML腳本標(biāo)簽構(gòu)建器
public class XMLScriptBuilder{
    // 標(biāo)簽節(jié)點(diǎn)處理器池
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

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

    // 動(dòng)態(tài)標(biāo)簽處理器
    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 初始化流程之后胡本,解析創(chuàng)建 SQL 源流程再分為兩步:

1、解析動(dòng)態(tài)標(biāo)簽畸悬,通過判斷每一塊動(dòng)態(tài)標(biāo)簽的類型侧甫,使用對應(yīng)的標(biāo)簽處理器進(jìn)行解析屬性和語句處理,并最終放置到混合 SQL 節(jié)點(diǎn)池中(MixedSqlNode)蹋宦,以供程序運(yùn)行時(shí)構(gòu)建 BoundSql 時(shí)使用披粟。

2、new SQL 源冷冗,根據(jù) SQL 是否有動(dòng)態(tài)標(biāo)簽或通配符占位符來確認(rèn)產(chǎn)生對象的靜態(tài)或動(dòng)態(tài) SQL 源守屉。

public SqlSource parseScriptNode() {
    
    // 1、解析動(dòng)態(tài)標(biāo)簽 蒿辙,并放到混合SQL節(jié)點(diǎn)池中
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    
    // 2拇泛、根據(jù)語句類型,new 出來最終的 SQL 源
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

原來解析動(dòng)態(tài)標(biāo)簽的工作交給了 parseDynamicTags() 方法须板,并且每一個(gè)語句對象的動(dòng)態(tài) SQL 標(biāo)簽最終都會被放到一個(gè)混合 SQL 節(jié)點(diǎn)池中碰镜。

// 混合 SQL 節(jié)點(diǎn)池
public class MixedSqlNode implements SqlNode {
    
    // 所有動(dòng)態(tài) SQL 標(biāo)簽:IF、WHERE习瑰、SET 等
    private final List<SqlNode> contents;
}

我們先看一下 SqlNode 接口的實(shí)現(xiàn)類绪颖,基本涵蓋了我們所有動(dòng)態(tài) SQL 標(biāo)簽處理器所需要使用到的節(jié)點(diǎn)實(shí)例。而其中混合 SQL 節(jié)點(diǎn) MixedSqlNode 作用僅是為了方便獲取每一個(gè)語句的所有動(dòng)態(tài)標(biāo)簽節(jié)點(diǎn)甜奄,于是應(yīng)勢而生柠横。

image

知道動(dòng)態(tài) SQL 標(biāo)簽節(jié)點(diǎn)處理器及以上的節(jié)點(diǎn)實(shí)現(xiàn)類之后,其實(shí)就能很容易理解课兄,到達(dá)程序運(yùn)行時(shí)牍氛,執(zhí)行器會調(diào)用 SQL 源來協(xié)助構(gòu)建 BoundSql 對象,而 SQL 源的核心工作烟阐,就是根據(jù)每一小段標(biāo)簽類型搬俊,匹配到對應(yīng)的節(jié)點(diǎn)實(shí)現(xiàn)類以解析拼接每一小段 SQL 語句紊扬。

程序運(yùn)行時(shí),動(dòng)態(tài) SQL 源獲取 BoundSql 對象 :

// 動(dòng)態(tài) SQL 源
public class DynamicSqlSource implements SqlSource { 
   
    // 這里的 rootSqlNode 屬性就是 MixedSqlNode 
    private final SqlNode rootSqlNode;
  
    @Override
    public BoundSql getBoundSql(Object parameterObject) {
 
        // 動(dòng)態(tài)SQL核心解析流程  
        rootSqlNode.apply(...);  
        
        return boundSql;

    } 
}

很明顯唉擂,通過調(diào)用 MixedSqlNode 的 apply () 方法餐屎,循環(huán)遍歷每一個(gè)具體的標(biāo)簽節(jié)點(diǎn)。

public class MixedSqlNode implements SqlNode {
    
      // 所有動(dòng)態(tài) SQL 標(biāo)簽:IF玩祟、WHERE腹缩、SET 等
      private final List<SqlNode> contents; 

      @Override
      public boolean apply(...) {

        // 循環(huán)遍歷,把每一個(gè)節(jié)點(diǎn)的解析分派到具體的節(jié)點(diǎn)實(shí)現(xiàn)之上
        // 例如 <if> 節(jié)點(diǎn)的解析交給 IfSqlNode
        // 例如 純文本節(jié)點(diǎn)的解析交給 StaticTextSqlNode
        contents.forEach(node -> node.apply(...));
        return true;
      }
}

我們選擇一兩個(gè)標(biāo)簽節(jié)點(diǎn)的解析過程進(jìn)行說明空扎,其它標(biāo)簽節(jié)點(diǎn)實(shí)現(xiàn)類的處理也基本雷同藏鹊。首先我們看一下 IF 標(biāo)簽節(jié)點(diǎn)的處理:

// IF 標(biāo)簽節(jié)點(diǎn)
public class IfSqlNode implements SqlNode { 
    
      private final ExpressionEvaluator evaluator;
    
      // 實(shí)現(xiàn)邏輯
      @Override
      public boolean apply(DynamicContext context) {
          
        // evaluator 是一個(gè)基于 OGNL 語法的解析校驗(yàn)類
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
          contents.apply(context);
          return true;
        }
        return false;
      } 
}

IF 標(biāo)簽節(jié)點(diǎn)的解析過程非常簡單,通過解析校驗(yàn)類 ExpressionEvaluator 來對 IF 標(biāo)簽的 test 屬性內(nèi)的表達(dá)式進(jìn)行解析校驗(yàn)转锈,滿足則拼接盘寡,不滿足則跳過。我們再看看 Trim 標(biāo)簽的節(jié)點(diǎn)解析過程黑忱,set 標(biāo)簽與 where 標(biāo)簽的底層處理都基于此:

public class TrimSqlNode implements SqlNode { 
    
    // 核心處理方法
    public void applyAll() {
        
        // 前綴智能補(bǔ)充與去除
        applyPrefix(..); 
        
        // 前綴智能補(bǔ)充與去除
        applySuffix(..); 
    } 
}

再來看一個(gè)純文本標(biāo)簽節(jié)點(diǎn)實(shí)現(xiàn)類的解析處理流程:

// 純文本標(biāo)簽節(jié)點(diǎn)實(shí)現(xiàn)類
public class StaticTextSqlNode implements SqlNode {
  
    private final String text;

    public StaticTextSqlNode(String text) {
        this.text = text;
    }
    
    // 節(jié)點(diǎn)處理宴抚,僅僅就是純粹的語句拼接
    @Override
    public boolean apply(DynamicContext context) {
        context.appendSql(text);
        return true;
    }
}

到這里,動(dòng)態(tài) SQL 的底層解析過程我們基本講解完甫煞,冗長了些,但流程上大致算完整冠绢,有遺漏的抚吠,我們回頭再補(bǔ)充。

image

總結(jié)

不知不覺中弟胀,我又是這么巨篇幅的講解剖析楷力,確實(shí)不太適合碎片化時(shí)間閱讀,不過話說回來孵户,畢竟此文屬于 Mybatis 全解系列萧朝,作為學(xué)研者還是建議深諳其中,對往后眾多框架技術(shù)的學(xué)習(xí)必有幫助夏哭。本文中我們很多動(dòng)態(tài) SQL 的介紹基本都使用 XML 配置方式检柬,當(dāng)然注解方式配置動(dòng)態(tài) SQL 也是支持的,動(dòng)態(tài) SQL 的語法書寫同 XML 方式竖配,但是需要在字符串前后添加 script 標(biāo)簽申明該語句為動(dòng)態(tài) 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);
    
}

此種動(dòng)態(tài) SQL 寫法可讀性較差,并且維護(hù)起來也挺硌手进胯,所以我個(gè)人是青睞 xml 方式配置語句用爪,一直追求解耦,大道也至簡胁镐。當(dāng)然偎血,也有很多團(tuán)隊(duì)和項(xiàng)目都在使用注解方式開發(fā)诸衔,這些沒有絕對,還是得結(jié)合自己的實(shí)際項(xiàng)目情況與團(tuán)隊(duì)等去做取舍颇玷。

本篇完笨农,本系列下一篇我們講《 Mybatis系列全解(九):Mybatis的復(fù)雜映射 》。

image
image

文章持續(xù)更新亚隙,微信搜索「潘潘和他的朋友們」第一時(shí)間閱讀磁餐,隨時(shí)有驚喜。本文會在 GitHub https://github.com/JavaWorld 收錄阿弃,關(guān)于熱騰騰的技術(shù)诊霹、框架、面經(jīng)渣淳、解決方案脾还、摸魚技巧、教程入愧、視頻鄙漏、漫畫等等等等,我們都會以最美的姿勢第一時(shí)間送達(dá)棺蛛,歡迎 Star ~ 我們未來 不止文章怔蚌!想進(jìn)讀者群的朋友歡迎撩我個(gè)人號:panshenlian,備注「加群」我們?nèi)豪飼沉模?BIU ~

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旁赊,一起剝皮案震驚了整個(gè)濱河市桦踊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌终畅,老刑警劉巖籍胯,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異离福,居然都是意外死亡杖狼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門妖爷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝶涩,“玉大人,你說我怎么就攤上這事赠涮∽釉ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵笋除,是天一觀的道長斜友。 經(jīng)常有香客問我,道長垃它,這世上最難降的妖魔是什么鲜屏? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任烹看,我火速辦了婚禮,結(jié)果婚禮上洛史,老公的妹妹穿的比我還像新娘惯殊。我一直安慰自己,他們只是感情好也殖,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布土思。 她就那樣靜靜地躺著,像睡著了一般忆嗜。 火紅的嫁衣襯著肌膚如雪己儒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天捆毫,我揣著相機(jī)與錄音闪湾,去河邊找鬼。 笑死绩卤,一個(gè)胖子當(dāng)著我的面吹牛途样,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播濒憋,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼何暇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凛驮?” 一聲冷哼從身側(cè)響起赖晶,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辐烂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捂贿,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纠修,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了厂僧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扣草。...
    茶點(diǎn)故事閱讀 38,577評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖颜屠,靈堂內(nèi)的尸體忽然破棺而出辰妙,到底是詐尸還是另有隱情,我是刑警寧澤甫窟,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布密浑,位于F島的核電站,受9級特大地震影響粗井,放射性物質(zhì)發(fā)生泄漏尔破。R本人自食惡果不足惜街图,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望懒构。 院中可真熱鬧餐济,春花似錦、人聲如沸胆剧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秩霍。三九已至篙悯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間前域,已是汗流浹背辕近。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匿垄,地道東北人移宅。 一個(gè)月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像椿疗,于是被迫代替她去往敵國和親漏峰。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容