日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪(fǎng)問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > 数据库 >内容正文

数据库

Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神!

發(fā)布時(shí)間:2024/3/13 数据库 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神! 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

封面:洛小汐

作者:潘潘

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

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

跨了個(gè)年感覺(jué)寫(xiě)了有半個(gè)世紀(jì) …

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

提前祝男神女神們?cè)届n越富越嗨森!

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

前言

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

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

Mybaits系列全解

  • Mybatis系列全解(一):手寫(xiě)一套持久層框架
  • Mybatis系列全解(二):Mybatis簡(jiǎn)介與環(huán)境搭建
  • Mybatis系列全解(三):Mybatis簡(jiǎn)單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注解開(kāi)發(fā)
  • Mybatis系列全解(十一):Mybatis緩存全解
  • Mybatis系列全解(十二):Mybatis插件開(kāi)發(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的底層原理

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 的全稱(chēng)是:

Structured Query Language,結(jié)構(gòu)化查詢(xún)語(yǔ)言。

SQL 本身好說(shuō),我們小學(xué)時(shí)候都學(xué)習(xí)過(guò)了,無(wú)非就是 CRUD 嘛,而且我們還知道它是一種 語(yǔ)言,語(yǔ)言是一種存在于對(duì)象之間用于交流表達(dá)的 能力,例如跟中國(guó)人交流用漢語(yǔ)、跟英國(guó)人交流用英語(yǔ)、跟火星人交流用火星語(yǔ)、跟小貓交流用喵喵語(yǔ)、跟計(jì)算機(jī)交流我們用機(jī)器語(yǔ)言、跟數(shù)據(jù)庫(kù)管理系統(tǒng)(DBMS)交流我們用 SQL。

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

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

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

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

其實(shí),上面我們已經(jīng)介紹過(guò),在例如 Java 高級(jí)語(yǔ)言中,都會(huì)嵌入(支持)SQL 能力,一般我們可以直接在代碼或配置文件中編寫(xiě) SQL 語(yǔ)句,如果一個(gè) SQL 語(yǔ)句在 “編譯階段” 就已經(jīng)能確定 主體結(jié)構(gòu),那我們稱(chēng)之為靜態(tài) SQL,如果一個(gè) SQL 語(yǔ)句在編譯階段無(wú)法確定主體結(jié)構(gòu),需要等到程序真正 “運(yùn)行時(shí)” 才能最終確定,那么我們稱(chēng)之為動(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),即查詢(xún) t_user 表的所有記錄,而無(wú)需等到程序運(yùn)行時(shí)才確定這個(gè)主體結(jié)構(gòu),因此以上屬于 靜態(tài) SQL。那我們?cè)倏纯聪旅孢@個(gè)語(yǔ)句:

<!-- 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); // 有 idUser user2 = new User(); sqlSession.select("dao.selectAll",user2); // 無(wú) id

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

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

<!-- 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 的定義,大家是否能判斷以上的語(yǔ)句塊是否屬于動(dòng)態(tài) SQL?

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

原因很簡(jiǎn)單,這個(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è)語(yǔ)句就是查詢(xún)一個(gè)指定 id 的用戶(hù)記錄,它最終執(zhí)行的 SQL 語(yǔ)句不會(huì)有任何動(dòng)態(tài)的變化,所以頂多算是一個(gè)支持動(dòng)態(tài)傳參的靜態(tài) SQL 。

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

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

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

那這么多好工具,都提供什么功能呢?相信我們平時(shí)接觸最多的就是接收?qǐng)?zhí)行 SQL 語(yǔ)句的輸入界面(也稱(chēng)為查詢(xún)編輯器),這個(gè)輸入界面幾乎支持所有 SQL 語(yǔ)法,例如我們編寫(xiě)一條語(yǔ)句查詢(xún) id 等于15 的用戶(hù)數(shù)據(jù)記錄:

select * from user where id = 15 ;

我們來(lái)看一下這個(gè)查詢(xún)結(jié)果:

很顯然,在這個(gè)輸入界面內(nèi)輸入的任何 SQL 語(yǔ)句,對(duì)于數(shù)據(jù)庫(kù)管理工具來(lái)說(shuō),都是 動(dòng)態(tài) SQL!因?yàn)楣ぞ弑旧聿⒉豢赡芴崆爸烙脩?hù)會(huì)輸入什么 SQL 語(yǔ)句,只有當(dāng)用戶(hù)執(zhí)行之后,工具才接收到用戶(hù)實(shí)際輸入的 SQL 語(yǔ)句,才能最終確定 SQL 語(yǔ)句的主體結(jié)構(gòu),當(dāng)然!即使我們不通過(guò)可視化的數(shù)據(jù)庫(kù)管理工具,也可以用數(shù)據(jù)庫(kù)本身自帶支持的命令行工具來(lái)執(zhí)行 SQL 語(yǔ)句。但無(wú)論用戶(hù)使用哪類(lèi)工具,輸入的語(yǔ)句都會(huì)被工具認(rèn)為是 動(dòng)態(tài) SQL

這么一說(shuō),動(dòng)態(tài) SQL 原來(lái)不是 Mybatis 獨(dú)有的特性!其實(shí)除了以上介紹的數(shù)據(jù)庫(kù)管理工具以外,在純 JDBC 時(shí)代,我們就經(jīng)常通過(guò)字符串來(lái)動(dòng)態(tài)的拼接 SQL 語(yǔ)句,這也是在高級(jí)語(yǔ)言環(huán)境(例如 Java 語(yǔ)言編程環(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í)行語(yǔ)句 connection.prepareStatement(sql);

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

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

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

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 語(yǔ)句并不能滿(mǎn)足我們復(fù)雜的業(yè)務(wù)場(chǎng)景需求,所以我們需要有適當(dāng)靈活的一套方式或者能力,來(lái)便捷高效的構(gòu)建動(dòng)態(tài) SQL 語(yǔ)句,去匹配我們動(dòng)態(tài)變化的業(yè)務(wù)需求。舉個(gè)栗子,在下面此類(lèi)多條件的場(chǎng)景需求之下,動(dòng)態(tài) SQL 語(yǔ)句就顯得尤為重要(先登場(chǎng) if 標(biāo)簽)。

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

那么, Mybatis 目前都提供了哪些棒棒的動(dòng)態(tài) SQL 標(biāo)簽?zāi)??我們先引出一個(gè)類(lèi)叫做 XMLScriptBuilder ,大家先簡(jiǎn)單理解它是負(fù)責(zé)解析我們的動(dòng)態(tài) SQL 標(biāo)簽的這么一個(gè)構(gòu)建器,在第4點(diǎn)底層原理中我們?cè)僭敿?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í)候,會(huì)實(shí)例化這么一個(gè)標(biāo)簽節(jié)點(diǎn)的構(gòu)造器,那么它本身就會(huì)提前把所有 Mybatis 支持的動(dòng)態(tài) SQL 標(biāo)簽對(duì)象對(duì)應(yīng)的處理器給進(jìn)行一個(gè)實(shí)例化,然后放到一個(gè) Map 池子里頭,而這些處理器,都是該類(lèi) XMLScriptBuilder 的一個(gè)匿名內(nèi)部類(lèi),而匿名內(nèi)部類(lèi)的功能也很簡(jiǎn)單,就是解析處理對(duì)應(yīng)類(lèi)型的標(biāo)簽節(jié)點(diǎn),在后續(xù)應(yīng)用程序使用動(dòng)態(tài)標(biāo)簽的時(shí)候,Mybatis 隨時(shí)到 Map 池子中匹配對(duì)應(yīng)的標(biāo)簽節(jié)點(diǎn)處理器,然后進(jìn)解析即可。下面我們分別對(duì)這 9 大動(dòng)態(tài) SQL 標(biāo)簽進(jìn)行介紹,排(gen)名(ju)不(wo)分(de)先(xi)后(hao):


Top1、if 標(biāo)簽

常用度:★★★★★

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

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

  • if 標(biāo)簽 : 內(nèi)嵌于 select / delete / update / insert 標(biāo)簽,如果滿(mǎn)足 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 語(yǔ)句,和 test 屬性搭配使用,通過(guò)判斷參數(shù)值來(lái)決定是否使用某個(gè)查詢(xún)條件,也可用于 Update 語(yǔ)句中判斷是否更新某個(gè)字段,或用于 Insert 語(yǔ)句中判斷是否插入某個(gè)字段的值。

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

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

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


Top2、choose 標(biāo)簽、when 標(biāo)簽、otherwise 標(biāo)簽

常用度:★★★★☆

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

有些時(shí)候,我們并不希望條件控制是非此即彼的,而是希望能提供多個(gè)條件并從中選擇一個(gè),所以貼心的 Mybatis 提供了 choose 標(biāo)簽元素,類(lèi)似我們 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ú)使用無(wú)意義
  • when 標(biāo)簽:內(nèi)嵌于 choose 標(biāo)簽之中,當(dāng)滿(mǎn)足某個(gè) when 條件時(shí),執(zhí)行對(duì)應(yīng)的代碼塊,并終止跳出 choose 標(biāo)簽,choose 中必須至少存在一個(gè) when 標(biāo)簽,否則無(wú)意義
  • otherwise 標(biāo)簽:內(nèi)嵌于 choose 標(biāo)簽之中,當(dāng)不滿(mǎn)足所有 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í),會(huì)執(zhí)行對(duì)應(yīng)的 when 標(biāo)簽內(nèi)的代碼塊,如果 when 標(biāo)簽的年齡 age 和姓名 name 都不滿(mǎn)足,則會(huì)拼接 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>

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

至于為何 choose 標(biāo)簽這么棒棒,而常用度還是比 if 標(biāo)簽少了一顆星呢?原因也簡(jiǎn)單,因?yàn)?choose 標(biāo)簽的很多使用場(chǎng)景可以直接用 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í)用性:★★★★☆

有些場(chǎng)景,可能需要查詢(xún) id 在 1 ~ 100 的用戶(hù)記錄

有些場(chǎng)景,可能需要批量插入 100 條用戶(hù)記錄

有些場(chǎng)景,可能需要更新 500 個(gè)用戶(hù)的姓名

有些場(chǎng)景,可能需要你刪除 10 條用戶(hù)記錄

請(qǐng)問(wèn)大家

很多增刪改查場(chǎng)景,操作對(duì)象都是集合/列表

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

額(⊙o⊙)…

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

沒(méi)錯(cuò),確實(shí) Mybatis 提供了 foreach 標(biāo)簽來(lái)處理這幾類(lèi)需要遍歷集合的場(chǎng)景,foreach 標(biāo)簽作為一個(gè)循環(huán)語(yǔ)句,他能夠很好的支持?jǐn)?shù)組、Map、或?qū)崿F(xiàn)了 Iterable 接口(List、Set)等,尤其是在構(gòu)建 in 條件語(yǔ)句的時(shí)候,我們常規(guī)的用法都是 id in (1,2,3,4,5 … 100) ,理論上我們可以在程序代碼中拼接字符串然后通過(guò) ${ ids } 方式來(lái)傳值獲取,但是這種方式不能防止 SQL 注入風(fēng)險(xiǎn),同時(shí)也特別容易拼接錯(cuò)誤,所以我們此時(shí)就需要使用 #{} + foreach 標(biāo)簽來(lái)配合使用,以滿(mǎn)足我們實(shí)際的業(yè)務(wù)需求。譬如我們傳入一個(gè) List 列表查詢(xún) id 在 1 ~ 100 的用戶(hù)記錄:

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

最終拼接完整的語(yǔ)句就變成:

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

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

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

最終拼接完整的語(yǔ)句就變成:

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

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

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

第一,當(dāng)傳入的參數(shù)為 List 對(duì)象時(shí),系統(tǒng)會(huì)默認(rèn)添加一個(gè) key 為 ‘list’ 的值,把列表內(nèi)容放到這個(gè) key 為 list 的集合當(dāng)中,在 foreach 標(biāo)簽中可以直接通過(guò) collection=“l(fā)ist” 獲取到 List 對(duì)象,無(wú)論你傳入時(shí)使用 kkk 或者 aaa ,都無(wú)所謂,系統(tǒng)都會(huì)默認(rèn)添加一個(gè) key 為 list 的值,并且 item 指定遍歷的對(duì)象值,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)會(huì)默認(rèn)添加一個(gè) key 為 ‘a(chǎn)rray’ 的值,把列表內(nèi)容放到這個(gè) key 為 array 的集合當(dāng)中,在 foreach 標(biāo)簽中可以直接通過(guò) collection=“array” 獲取到數(shù)組對(duì)象,無(wú)論你傳入時(shí)使用 ids 或者 aaa ,都無(wú)所謂,系統(tǒng)都會(huì)默認(rèn)添加一個(gè) key 為 array 的值,并且 item 指定遍歷的對(duì)象值,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 對(duì)象時(shí),系統(tǒng)并 不會(huì) 默認(rèn)添加一個(gè) key 值,需要手工傳入,例如傳入 key 值為 map2 的集合對(duì)象,在 foreach 標(biāo)簽中可以直接通過(guò) collection=“map2” 獲取到 Map 對(duì)象,并且 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>

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

  • 當(dāng)你傳入的參數(shù)是一個(gè) list 類(lèi)型時(shí),那么這個(gè)參數(shù)池子需要有一個(gè) key 值,以供上下文獲取這個(gè) list 類(lèi)型的對(duì)象,所以默認(rèn)設(shè)置了一個(gè) ‘list’ 字符串作為 key 值,獲取時(shí)通過(guò)使用 _parameter.list 來(lái)獲取,一般使用 list 即可。
  • 同樣的,當(dāng)你傳入的參數(shù)是一個(gè) array 數(shù)組時(shí),那么這個(gè)參數(shù)池子也會(huì)默認(rèn)設(shè)置了一個(gè) ‘a(chǎn)rray’ 字符串作為 key 值,以供上下文獲取這個(gè) array 數(shù)組的對(duì)象值,獲取時(shí)通過(guò)使用 _parameter.array 來(lái)獲取,一般使用 array 即可。
  • 但是!當(dāng)你傳入的參數(shù)是一個(gè) map 集合類(lèi)型時(shí),那么這個(gè)參數(shù)池就沒(méi)必要為你添加默認(rèn) key 值了,因?yàn)?map 集合類(lèi)型本身就會(huì)有很多 key 值,例如你想獲取 map 參數(shù)的某個(gè) key 值,你可以直接使用 _parameter.name 或者 _parameter.age 即可,就沒(méi)必要還用 _parameter.map.name 或者 _parameter.map.age ,所以這就是 map 參數(shù)類(lèi)型無(wú)需再構(gòu)建一個(gè) ‘map’ 字符串作為 key 的原因,對(duì)象類(lèi)型也是如此,例如你傳入一個(gè) User 對(duì)象。

因此,如果是 Map 集合,你可以這么使用:

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

直接使用 collection="_parameter",你會(huì)發(fā)現(xiàn)神奇的 key 和 value 都能通過(guò) _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>

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

那到這里,我們基本把 foreach 基本用法介紹完成,不過(guò)以上只是針對(duì)查詢(xún)的使用場(chǎng)景,對(duì)于刪除、更新、插入的用法,也是大同小異,我們簡(jiǎn)單說(shuō)一下,如果你希望批量插入 100 條用戶(hù)記錄:

<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è)用戶(hù)的姓名:

<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 條用戶(hù)記錄:

<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 標(biāo)簽時(shí),需要對(duì)傳入的 collection 參數(shù)(List/Map/Set等)進(jìn)行為空性判斷,否則動(dòng)態(tài) SQL 會(huì)出現(xiàn)語(yǔ)法異常,例如你的查詢(xún)語(yǔ)句可能是 select * from user where ids in () ,導(dǎo)致以上語(yǔ)法異常就是傳入?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)用開(kāi)發(fā)中常用度確實(shí)不分伯仲,二是這兩個(gè)標(biāo)簽出自一家,都繼承了 trim 標(biāo)簽,放置一起方便我們比對(duì)追根。(其中底層原理會(huì)在第4部分詳細(xì)講解)

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

<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 的寫(xiě)法,那 where 標(biāo)簽誕生之后,是怎么巧妙處理后續(xù)的條件語(yǔ)句的呢?

<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)軌蛑悄艿娜コ?#xff08;忽略)首個(gè)滿(mǎn)足條件語(yǔ)句的前綴,例如以上條件如果 age 和 name 都滿(mǎn)足,那么 age 前綴 and 會(huì)被智能去除掉,無(wú)論你是使用 and 運(yùn)算符或是 or 運(yùn)算符,Mybatis 框架都會(huì)幫你智能處理。

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

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

了解了基本用法之后,我們?cè)倏纯磩倓偽覀兊睦又?#xff1a;

<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 ,滿(mǎn)足 age != null 的條件之后,那么就會(huì)返回 where 標(biāo)簽并去除首個(gè)子句運(yùn)算符 and,最終的 SQL 語(yǔ)句會(huì)變成:

select * from User where age > 10; -- and 巧妙的不見(jiàn)了

值得注意的是,where 標(biāo)簽 只會(huì) 智能的去除(忽略)首個(gè)滿(mǎn)足條件語(yǔ)句的前綴,所以就建議我們?cè)谑褂?where 標(biāo)簽的時(shí)候,每個(gè)語(yǔ)句都最好寫(xiě)上 and 前綴或者 or 前綴,否則像以下寫(xiě)法就很有可能出大事:

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

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

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

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

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

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

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

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

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

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

根據(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>

很簡(jiǎn)單易懂,set 標(biāo)簽會(huì)智能拼接更新字段,以上例子如果傳入 age =10 和 username = ‘潘潘’ ,則有兩個(gè)字段滿(mǎn)足更新條件,于是 set 標(biāo)簽會(huì)智能拼接 " age = 10 ," 和 “username = ‘潘潘’ ,” 。其中由于后一個(gè) username 屬于最后一個(gè)子句,所以末尾逗號(hào)會(huì)被智能去除,最終的 SQL 語(yǔ)句是:

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

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

update user ; ( oh~ no!)

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

<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)有子元素滿(mǎn)足條件時(shí),除了 < !-- --> 注釋會(huì)被 set 忽略解析以外,其它注釋例如 // 或 /**/ 或 – 等都會(huì)被 set 標(biāo)簽當(dāng)成末尾子句元素處理,導(dǎo)致后續(xù)真正的末尾子句元素的逗號(hào)沒(méi)能被成功替換掉后綴,從而引起語(yǔ)法錯(cuò)誤!

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

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

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


Top5、trim 標(biāo)簽

常用度:★☆☆☆☆

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

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

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

  • prefix :前綴,當(dāng) trim 元素內(nèi)存在內(nèi)容時(shí),會(huì)給內(nèi)容插入指定前綴
  • suffix :后綴,當(dāng) trim 元素內(nèi)存在內(nèi)容時(shí),會(huì)給內(nèi)容插入指定后綴
  • prefixesToOverride :前綴去除,支持多個(gè),當(dāng) trim 元素內(nèi)存在內(nèi)容時(shí),會(huì)把內(nèi)容中匹配的前綴字符串去除。
  • suffixesToOverride :后綴去除,支持多個(gè),當(dāng) trim 元素內(nèi)存在內(nèi)容時(shí),會(huì)把內(nèi)容中匹配的后綴字符串去除。

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

<!--注意在使用 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)簽如果通過(guò) trim 標(biāo)簽實(shí)現(xiàn)的話(huà)可以這么編寫(xiě):

<trim prefix="SET" prefixOverrides="," >... </trim>或者<trim prefix="SET" suffixesToOverride="," >... </trim>

所以可見(jiàn) trim 是足夠靈活的,不過(guò)由于 where 標(biāo)簽和 set 標(biāo)簽這兩種 trim 標(biāo)簽變種方案已經(jīng)足以滿(mǎn)足我們實(shí)際開(kāi)發(fā)需求,所以直接使用 trim 標(biāo)簽的場(chǎng)景實(shí)際上不太很多(其實(shí)是我自己使用的不多,基本沒(méi)用過(guò))。

注意,set 標(biāo)簽之所以能夠支持去除前綴逗號(hào)或者后綴逗號(hà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í)包含了很多種場(chǎng)景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í)用性:★☆☆☆☆

簡(jiǎn)單來(lái)說(shuō),這個(gè)標(biāo)簽就是可以創(chuàng)建一個(gè)變量,并綁定到上下文,即供上下文使用,就是這樣,我把官網(wǎng)的例子直接拷貝過(guò)來(lái):

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

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

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

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

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

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

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

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

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

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

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

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

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


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

常用度:★★★☆☆

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

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

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

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

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

查詢(xún)或插入時(shí)簡(jiǎn)單復(fù)用:

<!-- 查詢(xún)時(shí)簡(jiǎn)單復(fù)用 --> <select id="selectUsers" resultType="map">select<include refid="userColumns"></include> from user </select><!-- 插入時(shí)簡(jiǎn)單復(fù)用 --> <insert id="insertUser" resultType="map">insert into user(<include refid="userColumns"></include> )values(#{id},#{username},#{password} ) </insert>

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

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

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

<!-- 查詢(xún)時(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)部語(yǔ)句中使用屬性值,屬性可以穿透?jìng)鬟f,例如:

<!-- 簡(jiǎn)單語(yǔ)句塊 --> <sql id="sql1">${prefix}_user </sql><!-- 嵌套語(yǔ)句塊 --> <sql id="sql2">from<include refid="${include_target}"/> </sql><!-- 查詢(xún)時(shí)引用嵌套語(yǔ)句塊 --> <select id="select" resultType="map">selectid, 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)簽的基本用法我們已介紹完畢,另外我們還有一些疑問(wèn):Mybatis 底層是如何解析這些動(dòng)態(tài) SQL 標(biāo)簽的呢?最終又是怎么構(gòu)建完整可執(zhí)行的 SQL 語(yǔ)句的呢?帶著這些疑問(wèn),我們?cè)诘?節(jié)中詳細(xì)分析。

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

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

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

我們的動(dòng)態(tài) SQL 解析部分就發(fā)生在 SQL 語(yǔ)句對(duì)象 MappedStatement 構(gòu)建時(shí)(上左高亮橘色部分,注意觀(guān)察其中 SQL 語(yǔ)句對(duì)象與 SqlSource 、 BoundSql 的關(guān)系,在動(dòng)態(tài) SQL 解析流程特別關(guān)鍵)。我們?cè)倮稽c(diǎn),可以看到無(wú)論是使用 XML 配置 SQL 語(yǔ)句或是使用注解方式配置 SQL 語(yǔ)句,框架最終都會(huì)把解析完成的 SQL 語(yǔ)句對(duì)象存放到 MappedStatement 語(yǔ)句集合池子。

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

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

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

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

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

  • 第二,注解方式配置 SQL 語(yǔ)句,框架如何解析?

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

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

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

這一切,得從你編寫(xiě)的 SQL 開(kāi)始講起 …

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

// SQL 語(yǔ)句對(duì)象 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è)特別的屬性就是我們的語(yǔ)句源 SqlSource ,功能純粹也恰如其名 SQL 源。它是一個(gè)接口,它會(huì)結(jié)合用戶(hù)傳遞的參數(shù)對(duì)象 parameterObject 與動(dòng)態(tài) SQL,生成 SQL 語(yǔ)句,并最終封裝成 BoundSql 對(duì)象。SqlSource 接口有5個(gè)實(shí)現(xiàn)類(lèi),分別是:StaticSqlSource、DynamicSqlSource、RawSqlSource、ProviderSqlSource、VelocitySqlSource (而 velocitySqlSource 目前只是一個(gè)測(cè)試用例,還沒(méi)有用作實(shí)際的 Sql 源實(shí)現(xiàn))。

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

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

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

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

public interface SqlSource {BoundSql getBoundSql(Object parameterObject);}

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

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

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

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

墻裂推薦閱讀之前第六文之 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í),框架會(huì)根據(jù) SQL 語(yǔ)句類(lèi)型構(gòu)建對(duì)應(yīng)的 SqlSource 源實(shí)例(靜態(tài)/動(dòng)態(tài)).
  • 程序?qū)嶋H運(yùn)行時(shí),框架會(huì)根據(jù)傳入?yún)?shù)動(dòng)態(tài)的構(gòu)建 BoundSql 對(duì)象,輸送最終 SQL 到數(shù)據(jù)庫(kù)執(zhí)行。

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

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

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

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

<select id="findAll" resultType="map" lang="RAW" >select * from user </select><!-- 別名或全限定類(lèi)名都允許 --><select id="findAll" resultType="map" lang="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver">select * from user </select>

框架允許我們通過(guò) lang 屬性手工指定語(yǔ)言驅(qū)動(dòng),不指定則系統(tǒng)默認(rèn)是 lang = “XML”,XML 代表 XMLLanguageDriver ,當(dāng)然 lang 屬性可以是我們內(nèi)置的別名也可以是我們的語(yǔ)言驅(qū)動(dòng)全限定名,不過(guò)值得注意的是,當(dāng)語(yǔ)句中含有動(dòng)態(tài) SQL 標(biāo)簽時(shí),就只能選擇使用 lang=“XML”,否則程序在初始化構(gòu)件時(shí)就會(huì)報(bào)錯(cuò)。

## Cause: org.apache.ibatis.builder.BuilderException: ## Dynamic content is not allowed when using RAW language ## 動(dòng)態(tài)語(yǔ)句內(nèi)容不被原生語(yǔ)言驅(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)初始階段,語(yǔ)言驅(qū)動(dòng)創(chuàng)建 SQL 源,而運(yùn)行時(shí), SQL 源動(dòng)態(tài)解析構(gòu)建出 BoundSql 。

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

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

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

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

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

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

而在前面我們就已經(jīng)介紹, XMLScriptBuilder 實(shí)例初始構(gòu)造時(shí),會(huì)初始構(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)簽,通過(guò)判斷每一塊動(dòng)態(tài)標(biāo)簽的類(lèi)型,使用對(duì)應(yīng)的標(biāo)簽處理器進(jìn)行解析屬性和語(yǔ)句處理,并最終放置到混合 SQL 節(jié)點(diǎn)池中(MixedSqlNode),以供程序運(yùn)行時(shí)構(gòu)建 BoundSql 時(shí)使用。

2、new SQL 源,根據(jù) SQL 是否有動(dòng)態(tài)標(biāo)簽或通配符占位符來(lái)確認(rèn)產(chǎn)生對(duì)象的靜態(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ù)語(yǔ)句類(lèi)型,new 出來(lái)最終的 SQL 源SqlSource sqlSource;if (isDynamic) {sqlSource = new DynamicSqlSource(configuration, rootSqlNode);} else {sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);}return sqlSource; }

原來(lái)解析動(dòng)態(tài)標(biāo)簽的工作交給了 parseDynamicTags() 方法,并且每一個(gè)語(yǔ)句對(duì)象的動(dòng)態(tài) SQL 標(biāo)簽最終都會(huì)被放到一個(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)類(lèi),基本涵蓋了我們所有動(dòng)態(tài) SQL 標(biāo)簽處理器所需要使用到的節(jié)點(diǎn)實(shí)例。而其中混合 SQL 節(jié)點(diǎn) MixedSqlNode 作用僅是為了方便獲取每一個(gè)語(yǔ)句的所有動(dòng)態(tài)標(biāo)簽節(jié)點(diǎn),于是應(yīng)勢(shì)而生。

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

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

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

很明顯,通過(guò)調(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; @Overridepublic boolean apply(...) {// 循環(huán)遍歷,把每一個(gè)節(jié)點(diǎn)的解析分派到具體的節(jié)點(diǎn)實(shí)現(xiàn)之上// 例如 <if> 節(jié)點(diǎn)的解析交給 IfSqlNode// 例如 純文本節(jié)點(diǎn)的解析交給 StaticTextSqlNodecontents.forEach(node -> node.apply(...));return true;} }

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

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

IF 標(biāo)簽節(jié)點(diǎn)的解析過(guò)程非常簡(jiǎn)單,通過(guò)解析校驗(yàn)類(lèi) ExpressionEvaluator 來(lái)對(duì) IF 標(biāo)簽的 test 屬性?xún)?nèi)的表達(dá)式進(jìn)行解析校驗(yàn),滿(mǎn)足則拼接,不滿(mǎn)足則跳過(guò)。我們?cè)倏纯?Trim 標(biāo)簽的節(jié)點(diǎn)解析過(guò)程,set 標(biāo)簽與 where 標(biāo)簽的底層處理都基于此:

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

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

// 純文本標(biāo)簽節(jié)點(diǎn)實(shí)現(xiàn)類(lèi) public class StaticTextSqlNode implements SqlNode {private final String text;public StaticTextSqlNode(String text) {this.text = text;}// 節(jié)點(diǎn)處理,僅僅就是純粹的語(yǔ)句拼接@Overridepublic boolean apply(DynamicContext context) {context.appendSql(text);return true;} }

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

總結(jié)

不知不覺(jué)中,我又是這么巨篇幅的講解剖析,確實(shí)不太適合碎片化時(shí)間閱讀,不過(guò)話(huà)說(shuō)回來(lái),畢竟此文屬于 Mybatis 全解系列,作為學(xué)研者還是建議深諳其中,對(duì)往后眾多框架技術(shù)的學(xué)習(xí)必有幫助。本文中我們很多動(dòng)態(tài) SQL 的介紹基本都使用 XML 配置方式,當(dāng)然注解方式配置動(dòng)態(tài) SQL 也是支持的,動(dòng)態(tài) SQL 的語(yǔ)法書(shū)寫(xiě)同 XML 方式,但是需要在字符串前后添加 script 標(biāo)簽申明該語(yǔ)句為動(dòng)態(tài) SQL ,例如:

public class UserDao {/*** 更新用戶(hù)*/@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 寫(xiě)法可讀性較差,并且維護(hù)起來(lái)也挺硌手,所以我個(gè)人是青睞 xml 方式配置語(yǔ)句,一直追求解耦,大道也至簡(jiǎn)。當(dāng)然,也有很多團(tuán)隊(duì)和項(xiàng)目都在使用注解方式開(kāi)發(fā),這些沒(méi)有絕對(duì),還是得結(jié)合自己的實(shí)際項(xiàng)目情況與團(tuán)隊(duì)等去做取舍。

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

文章持續(xù)更新,微信搜索「潘潘和他的朋友們」第一時(shí)間閱讀,隨時(shí)有驚喜。本文會(huì)在 GitHub https://github.com/JavaWorld 收錄,關(guān)于熱騰騰的技術(shù)、框架、面經(jīng)、解決方案、摸魚(yú)技巧、教程、視頻、漫畫(huà)等等等等,我們都會(huì)以最美的姿勢(shì)第一時(shí)間送達(dá),歡迎 Star ~ 我們未來(lái) 不止文章!想進(jìn)讀者群的朋友歡迎撩我個(gè)人號(hào):panshenlian,備注「加群」我們?nèi)豪飼沉?#xff0c; BIU ~

總結(jié)

以上是生活随笔為你收集整理的Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。

天天干天天射天天操 | 亚洲每日更新 | 日韩电影在线观看一区二区三区 | 久久免费看毛片 | 国产精品亚洲视频 | 中文字幕影视 | 五月开心综合 | 美女黄色网在线播放 | 国产精品永久久久久久久www | 人人超碰在线 | 美女搞黄国产视频网站 | 国内久久久久久 | 伊人成人久久 | 少妇性bbb搡bbb爽爽爽欧美 | 婷婷色网站 | 久久久精品一区二区三区 | 精品美女在线视频 | 亚洲视频久久久久 | 国产精品毛片一区二区 | 亚洲欧洲精品视频 | 97国产超碰| 中文字幕在线观看一区二区三区 | 欧美午夜a | 天天骚夜夜操 | av直接看 | 国产精品1区 | 91亚洲精品乱码久久久久久蜜桃 | 国产 日韩 欧美 中文 在线播放 | 国产二区电影 | 久久婷五月| 亚洲片在线资源 | 少妇av网 | av 一区二区三区四区 | 视频三区在线 | 日韩欧美一区二区三区黑寡妇 | 久久久久久久久久久久电影 | 成人小视频在线观看免费 | 国产小视频在线播放 | 黄色片视频免费 | 国内免费久久久久久久久久久 | 免费男女网站 | 天天操天天操天天操天天操 | 手机在线黄色网址 | 精品国产免费av | 免费网站看v片在线a | 中文字幕在线国产精品 | 日韩久久视频 | 黄色av网站在线免费观看 | 日韩理论在线视频 | 色婷婷狠狠五月综合天色拍 | 国产精品欧美精品 | 日韩中文字幕免费在线观看 | 欧美福利久久 | 欧美乱码精品一区 | 激情综合久久 | 国产美女网 | 国产一区二区中文字幕 | 男女全黄一级一级高潮免费看 | 色噜噜在线观看 | 久久久999精品视频 国产美女免费观看 | 免费视频 三区 | 福利网址在线观看 | 国产精品成人免费一区久久羞羞 | 亚洲欧美日本国产 | 在线欧美中文字幕 | 国产大陆亚洲精品国产 | 日批视频在线观看免费 | 亚州精品国产 | 一级性视频 | 日韩中文字幕免费视频 | 精品产品国产在线不卡 | 二区在线播放 | 日日干网 | 欧美成天堂网地址 | 欧美在线free | www.久久色| 最新中文字幕在线资源 | 亚洲经典中文字幕 | 欧美另类xxx| 欧美一级欧美一级 | 香蕉视频色 | 国产在线播放一区 | 亚洲精品高清在线 | 99视频这里有精品 | 99热这里精品 | 福利视频第一页 | 午夜视频在线观看一区二区 | 久精品在线 | 国产涩图 | 日韩a在线看| 激情婷婷综合网 | 91av在线免费视频 | 超碰97在线看| 四虎在线观看网址 | 特片网久久 | 国产亚洲一区二区在线观看 | 国产视频1 | 在线观看亚洲国产精品 | 国产96视频| 色爱区综合激月婷婷 | 91视频啪| 五月丁色 | 热久久视久久精品18亚洲精品 | 中文字幕色婷婷在线视频 | 国产精品国产三级国产aⅴ无密码 | 99精品国产成人一区二区 | 亚洲精品一区二区三区在线观看 | 99tvdz@gmail.com | 亚洲午夜剧场 | 天天天天天天天天操 | 欧美日韩国产在线一区 | 国产精品区一区 | 久久午夜国产 | 麻花豆传媒mv在线观看网站 | 91中文字幕永久在线 | 免费69视频| 日韩专区在线播放 | 午夜久久久久久久久久影院 | 免费av福利 | 国产精品一区二区在线免费观看 | 超碰伊人网 | 91丨九色丨蝌蚪丰满 | 久久视了 | 中文字幕视频在线播放 | 免费在线观看av不卡 | 久久理论电影 | 久久爱导航 | 亚洲九九 | 精品久久久久久久久久久久久久久久 | 国产精品24小时在线观看 | a级国产乱理伦片在线播放 久久久久国产精品一区 | 国产午夜精品久久久久久久久久 | 中文字幕资源在线观看 | 91欧美国产 | 在线免费91 | av直接看| 亚洲无线视频 | 日韩视频一区二区三区在线播放免费观看 | 久久综合操 | 免费看成年人 | 日韩欧美电影在线 | 成人av电影免费在线观看 | 中文视频在线看 | 超碰97人人在线 | 九九99 | 中文视频在线看 | 青草视频在线免费 | 国产综合激情 | 99热9| 韩日精品在线 | 亚洲妇女av | 黄色软件大全网站 | 亚洲精选国产 | 国产精品久久久av | 成人蜜桃视频 | 日本黄色黄网站 | 久久国产精品免费一区二区三区 | 国产高清视频在线免费观看 | 高潮久久久 | 四虎影视精品 | 极品嫩模被强到高潮呻吟91 | 久久精品5 | 波多野结衣精品视频 | www国产精品com | av免费福利| 国产色婷婷在线 | 日韩久久久久久久久 | 久久影视网 | 日本午夜在线亚洲.国产 | 久久久一本精品99久久精品66 | 在线色亚洲 | 中文在线字幕观看电影 | 亚洲自拍偷拍色图 | 少妇搡bbb | 超碰97公开 | 久在线观看视频 | 中文字幕在线视频一区二区三区 | 欧美日韩午夜 | 日韩高清一二三区 | 欧美性久久久久久 | 麻豆传媒精品 | 超碰在线人人艹 | 精品乱码一区二区三四区 | 在线免费观看黄色小说 | 亚洲欧美日韩精品久久久 | 91高清免费观看 | 91mv.cool在线观看 | 国产久视频| 久久久久久国产精品免费 | 国产精品久久久免费看 | 久久国产高清 | 日韩av成人在线观看 | 午夜久久久精品 | 又黄又刺激 | 人人干天天射 | 国产精品欧美一区二区三区不卡 | 国产精品日韩 | 三级小视频在线观看 | 日日夜夜天天 | 亚洲精品资源在线 | 国产精品青青 | 国产男女爽爽爽免费视频 | 天天操天天摸天天射 | 亚洲精品中文在线资源 | 日本精品一 | 日韩精品在线免费观看 | 中文字幕av电影下载 | 77国产精品 | mm1313亚洲精品国产 | 亚洲高清视频在线播放 | 久久香蕉电影网 | 成人av网站在线播放 | 日韩在线中文字幕视频 | 国产精品日韩在线 | 国产精品第三页 | 欧美色伊人 | 欧美激情视频免费看 | 久草在线视频免费资源观看 | 国产精品99久久久精品免费观看 | 波多野结衣久久资源 | 日韩激情视频 | 久久精品久久综合 | 国产不卡在线 | 亚洲人人网| 超碰在线99 | 婷婷午夜天| 久久国产成人午夜av影院潦草 | 中文字幕在线视频一区二区 | 久久福利电影 | 亚洲精品美女在线 | 三级黄免费看 | 婷婷去俺也去六月色 | 四虎www com| 精品成人a区在线观看 | 日日操日日插 | 伊人天天干 | 国产亚洲激情视频在线 | 国产97在线播放 | 狠狠操91| 黄色一级性片 | 最新成人在线 | 中文字幕av最新更新 | 久久精品电影院 | 久久久久欧美精品999 | 国内精品久久久久久久影视麻豆 | 国产成人精品av久久 | 韩日精品中文字幕 | 国产精品嫩草在线 | 综合网久久 | 日韩久久精品一区二区三区下载 | 久久久久久视频 | 成片免费观看视频999 | av色综合网 | 99色亚洲| 日韩性xxx| 蜜臀久久99精品久久久久久网站 | 日韩视频免费观看高清 | 国产成人精品久久久 | www.国产毛片 | 日韩簧片在线观看 | 亚洲精品欧美专区 | 婷婷色九月| 婷婷激情在线观看 | 国内精品久久久久 | 欧美日韩电影在线播放 | 免费看黄在线网站 | 国内揄拍国产精品 | 又黄又爽又色无遮挡免费 | 天天草天天色 | 亚洲高清免费在线 | 久久精品导航 | 午夜av在线免费 | 最近最新最好看中文视频 | 精品一二区 | 亚洲成av人片在线观看无 | 狠狠狠色丁香综合久久天下网 | av电影中文字幕在线观看 | 日韩精品免费一线在线观看 | 99久久精品免费看国产麻豆 | 美女av免费 | 五月综合在线观看 | 国产一区二区精品在线 | 97精品久久| 国产专区在线看 | 99精品视频观看 | 成人 亚洲 欧美 | 欧美激情视频一区二区三区免费 | 免费看一级黄色大全 | 色在线最新 | 国产日韩欧美视频在线观看 | 国产亚洲欧美在线视频 | 在线观看日韩一区 | h视频在线看| 黄色在线观看网站 | 男女视频91| 色爱成人网| 一区二区三区在线免费观看视频 | 最新成人av | 国产传媒中文字幕 | 欧美激情在线网站 | 亚洲精品动漫成人3d无尽在线 | 国内精品久久久久 | 日韩欧美xx | 亚洲国产成人精品电影在线观看 | 久久国产精品影片 | 亚洲国产中文字幕在线视频综合 | 国产亚洲久一区二区 | 狠狠色香婷婷久久亚洲精品 | 国产精品综合在线 | 美女天天操 | 日韩视频欧美视频 | 日本护士三级少妇三级999 | 国产视频日韩视频欧美视频 | 深夜福利视频一区二区 | 在线观看你懂的网址 | 国产精品国产三级国产专区53 | 视频在线一区 | 国产综合精品久久 | 日本黄色免费在线观看 | 99tvdz@gmail.com| 国产高清视频色在线www | 精品一区在线 | 九九九免费视频 | 国产专区欧美专区 | 波多在线视频 | 久久精品精品电影网 | 久久久亚洲麻豆日韩精品一区三区 | 婷婷六月天丁香 | 国产精品女主播一区二区三区 | 中文字幕视频网站 | 久草久 | 欧美xxxx性xxxxx高清 | 中文字幕在线免费看 | 午夜美女福利直播 | 狠狠色丁香婷婷综合最新地址 | 久久五月天综合 | av成人黄色 | 中文字幕av最新 | 亚洲激情视频 | 精品v亚洲v欧美v高清v | 黄色大片视频网站 | 久久久久久久久久久综合 | 色偷偷男人的天堂av | 懂色av一区二区在线播放 | 中文字幕二区在线观看 | 一区二区三区国 | 亚洲日本欧美在线 | 最新免费中文字幕 | 国产精品免费在线 | 99国产成+人+综合+亚洲 欧美 | 亚洲免费精品视频 | 亚洲激情小视频 | 国产99爱 | 久久噜噜少妇网站 | 精精国产xxxx视频在线播放 | 玖玖精品在线 | 亚洲精品国产精品国自 | a天堂最新版中文在线地址 久久99久久精品国产 | 欧美亚洲精品在线观看 | 91cn国产在线 | 成人三级视频 | 午夜精品麻豆 | 日韩在线电影观看 | 国产精品久久久久久久久久久久午 | 免费看三级黄色片 | 99精品视频在线观看播放 | 婷婷精品在线 | 欧美在线资源 | 欧美精品亚洲精品日韩精品 | 久久久午夜视频 | 国内精品亚洲 | 中文字幕在线人 | 超碰97在线看 | 亚洲男模gay裸体gay | 亚州精品视频 | 国产精品综合久久久久久 | 99久久精品久久久久久动态片 | 开心激情婷婷 | 久久久久久久久久久久国产精品 | 久久久久免费电影 | 中文字幕 国产视频 | 日韩a级黄色 | 中文字幕在线专区 | 免费毛片一区二区三区久久久 | 免费看三级网站 | 色噜噜狠狠色综合中国 | 在线天堂中文www视软件 | 狠狠干狠狠久久 | 最近最新mv字幕免费观看 | 久草视频视频在线播放 | 特黄一级毛片 | 亚洲理论电影网 | 国产在线不卡精品 | 91在线porny国产在线看 | 天天色天天综合网 | 久久电影日韩 | 亚州精品天堂中文字幕 | 啪啪精品 | 99精品国产高清在线观看 | 日韩三级不卡 | 日本中文字幕在线免费观看 | 天天综合网天天综合色 | 最新不卡av | 亚洲欧洲中文日韩久久av乱码 | 国产一级做a | 天天色中文 | 国产高清黄色 | 亚洲第五色综合网 | 激情五月av | www欧美日韩| 亚洲美女免费视频 | 亚洲精品美女 | 人人爽久久久噜噜噜电影 | 日批视频 | 欧美视频xxx| 在线观看视频福利 | 黄色片视频在线观看 | 99久热在线精品视频观看 | 不卡在线一区 | 成人黄色在线电影 | 亚洲最新视频在线 | 日韩美女av在线 | 日日夜夜亚洲 | 国产午夜精品一区二区三区四区 | 国产精品永久在线观看 | avlulu久久精品| 99视频久| 亚洲激情在线视频 | 999成人| 黄污视频大全 | 麻豆极品| 99久久99视频只有精品 | 日日操网站 | 在线观看一二三区 | 狠狠干狠狠插 | 日韩精品一区二区三区中文字幕 | 香蕉视频4aa | 欧美精品一级视频 | 国产在线精品播放 | 欧美日韩高清在线观看 | 一区二区三区四区免费视频 | 久久经典视频 | 98精品国产自产在线观看 | 狠狠色狠狠色合久久伊人 | 精品国产一区二区三区久久影院 | 丁香网五月天 | 日韩欧美综合视频 | 亚洲精品福利在线 | 免费av大全| 成人精品一区二区三区中文字幕 | 久久99精品国产99久久 | 在线色亚洲 | 91久久一区二区 | a电影在线观看 | 国产又粗又猛又黄又爽视频 | 国产老熟 | 久久夜色精品国产欧美乱 | 人人超碰人人 | 日本大尺码专区mv | 久操中文字幕在线观看 | 九九热中文字幕 | 久久综合久久综合久久 | 中文字幕在线色 | 久久国产亚洲精品 | 中文字幕久久精品 | av在线短片 | www久久| 9ⅰ精品久久久久久久久中文字幕 | 中文字幕视频三区 | 精品福利国产 | 久草青青在线观看 | 国产高清在线免费观看 | 欧美日韩另类在线观看 | 久久精品视频中文字幕 | 免费精品久久久 | 六月丁香社区 | 中文字幕av免费在线观看 | 九九免费精品视频 | 亚洲影院一区 | 国产欧美日韩精品一区二区免费 | 久久精品99国产精品 | www最近高清中文国语在线观看 | 亚洲aⅴ免费在线观看 | 久久夜色精品亚洲噜噜国4 午夜视频在线观看欧美 | 国产日韩视频在线 | 美女网站色免费 | 91在线视频观看免费 | 久久国产精品久久久久 | 综合久久久久久久久 | 亚洲四虎在线 | 亚洲一区二区三区91 | 91桃色在线观看视频 | 欧美日韩在线视频观看 | 亚洲精品456在线播放 | 九色porny真实丨国产18 | 亚洲免费av在线播放 | 青青河边草观看完整版高清 | 最新色站 | 日本久久91 | 中文字幕一区在线观看视频 | 国产在线传媒 | www视频免费在线观看 | 韩国精品在线观看 | 国产精品刺激对白麻豆99 | 五月天综合网站 | 国产黄在线免费观看 | 欧美日韩中文字幕在线视频 | 国产精品欧美一区二区 | 亚洲一级二级 | 在线免费观看国产精品 | 成人免费看电影 | 午夜婷婷在线播放 | 国内精品久久久久久中文字幕 | 91激情视频在线播放 | 97人人人人| 人人干人人干人人干 | 免费合欢视频成人app | 国产精品麻豆果冻传媒在线播放 | 婷婷精品国产欧美精品亚洲人人爽 | 人人要人人澡人人爽人人dvd | 日韩一级电影网站 | 伊人干综合| 亚洲三级在线免费观看 | 亚洲精品大全 | 国产不卡一二三区 | 国产精品大片免费观看 | 国产免费高清 | 国产xxxx| 国内精品亚洲 | 1000部18岁以下禁看视频 | 在线免费色视频 | 亚州国产精品久久久 | 久视频在线 | av大全免费在线观看 | 超碰免费成人 | 国产一区二区不卡视频 | 天天激情综合 | 日韩精品一区二区三区丰满 | 青青河边草免费直播 | 免费看精品久久片 | 久久天天躁夜夜躁狠狠85麻豆 | 毛片无卡免费无播放器 | 91在线免费观看网站 | 狠狠色狠狠色综合日日92 | 国产精品1区2区3区在线观看 | 特级西西444www大胆高清无视频 | 中文字幕免费观看 | 夜夜爽88888免费视频4848 | 狠狠天天 | 色综合久久久久久久久五月 | 国产亚洲精品美女 | 人人澡人人爽欧一区 | 国产视频一区二区在线播放 | 午夜精品视频福利 | 国产九九精品视频 | 成年人在线 | 在线观看完整版免费 | 最近2019好看的中文字幕免费 | 波多野结衣精品在线 | 国产最新精品视频 | 亚洲欧美综合 | av综合网址 | 99精品系列 | 午夜私人影院久久久久 | 欧美一区二区免费在线观看 | 欧美一级小视频 | 国产一级片播放 | 中文在线免费一区三区 | 欧美污污视频 | 97天天综合网 | 国产精品久久久久久久久婷婷 | 91福利视频网站 | 成年人在线看片 | 97超碰人人模人人人爽人人爱 | 欧美午夜久久久 | 97成人精品区在线播放 | 久久精品视频在线免费观看 | 色婷婷a | 一区二区日韩av | 色视频国产直接看 | 黄色av电影网 | 在线欧美中文字幕 | 97看片网 | 国产精品久久人 | 99精品视频观看 | 久久国产精品免费看 | 亚洲激情一区二区三区 | 精品视频免费播放 | 夜添久久精品亚洲国产精品 | 久久精品日本啪啪涩涩 | 99久久免费看 | 国产日韩精品在线 | 高清国产在线一区 | 免费一级片久久 | 国产一卡二卡在线 | 九色91福利 | 免费一级特黄录像 | 麻豆久久久久 | 亚洲在线精品 | 日韩在线观看影院 | 国产精品初高中精品久久 | 中文在线免费看视频 | 久久精品香蕉视频 | 免费观看第二部31集 | 日韩欧美在线观看一区二区三区 | 欧美九九九 | sesese图片 | 九九导航 | 久草在线视频精品 | 国产视频久久 | 在线91网| 久久激情五月丁香伊人 | 人人精品久久 | 久久精品国产免费看久久精品 | 日韩在线免费播放 | 特级西西444www高清大视频 | 亚洲国产无 | 8x8x在线观看视频 | 久久精品这里精品 | 久久久精品免费观看 | 粉嫩av一区二区三区四区五区 | 五月婷视频 | 免费在线色视频 | 欧美性成人 | 久久夜色电影 | 久久精品在线视频 | 日韩偷拍精品 | 四虎国产免费 | 91视频 - 88av | 午夜狠狠干| 久久99精品久久只有精品 | 91亚洲视频在线观看 | 国产精品综合久久 | 久久夜夜夜 | 国产在线中文字幕 | av成人免费在线看 | 国产最新在线视频 | 精品国产免费观看 | 在线观看免费视频 | 五月婷亚洲 | 免费看国产黄色 | 超碰精品在线 | 亚洲高清激情 | 欧美日韩激情视频8区 | 日本在线h| 久久久午夜精品理论片中文字幕 | 亚洲第一av在线播放 | 九月婷婷色 | 成人a免费视频 | 国产精品久久久久毛片大屁完整版 | 六月色婷| 一区二区三区免费在线观看视频 | 美女久久久久久久久久久 | 国产精品欧美久久久久三级 | 99久久精品国产系列 | 毛片基地黄久久久久久天堂 | 亚洲综合激情小说 | 天天色天天干天天色 | 国产又粗又猛又黄又爽 | 亚洲美女视频在线 | 色偷偷人人澡久久超碰69 | 亚洲欧美成人综合 | 国模视频一区二区三区 | 国产资源在线免费观看 | 在线性视频日韩欧美 | 在线观看国产www | 91av网址| 一级黄色a视频 | 亚洲精品在线电影 | 天天艹天天爽 | 97视频在线免费播放 | 国产视频久久久久 | 欧美嫩草影院 | 欧美一区在线观看视频 | 国内精品视频一区二区三区八戒 | 日韩电影在线看 | 欧美日韩一级久久久久久免费看 | 在线免费观看不卡av | 久久久久久久久久久黄色 | 亚洲欧美国产精品18p | 国产精品永久免费观看 | 日韩在线不卡视频 | 国产精品观看视频 | 草莓视频在线观看免费观看 | 国产视频观看 | 美女视频国产 | 国产黄在线免费观看 | 成人中文字幕在线观看 | 亚洲视频,欧洲视频 | 91视频-88av| 少妇18xxxx性xxxx片 | 黄色的视频 | 色婷婷成人 | 欧美成人h版 | 日韩高清免费无专码区 | 欧美日韩精品影院 | 91精品国产欧美一区二区成人 | 中文亚洲欧美日韩 | 国产在线精品观看 | 97超碰资源总站 | 免费黄色av片 | 国产精品系列在线播放 | 久久久精品亚洲 | 日韩中文字幕第一页 | 国产精品一区二区三区视频免费 | 在线观看免费观看在线91 | 99操视频 | 亚洲综合色视频 | 亚洲最大的av网站 | 国产在线一区二区 | 精品久久久久久一区二区里番 | 欧美在线free | 国产亚洲视频在线免费观看 | 久久精品综合网 | 成人资源在线观看 | 视频二区在线视频 | 欧美电影在线观看 | 国产午夜视频在线观看 | 国产成人精品一区二区三区网站观看 | 青青久视频 | 亚洲免费专区 | 天天干,夜夜爽 | 久热久草 | 天天综合色天天综合 | 国产中文字幕网 | 深爱激情婷婷网 | 最近中文字幕完整视频高清1 | 久久久久蜜桃 | 日韩免费小视频 | 亚洲乱码国产乱码精品天美传媒 | 免费a级大片 | 成人精品久久 | 亚洲国产日韩欧美 | 国内一级片在线观看 | 三级av小说 | 狠狠干夜夜操天天爽 | 成人免费亚洲 | 欧美激情精品久久久久久免费 | 91综合久久一区二区 | 国产91影视| 亚洲国产精品999 | 欧美另类激情 | 国产国产人免费人成免费视频 | 天天色欧美 | 久久久久久综合 | 成人一区二区三区中文字幕 | 在线欧美最极品的av | 五月天婷婷在线播放 | 国产精品福利在线 | 欧美日韩电影在线播放 | 日韩大片在线播放 | 黄色网址在线播放 | 91九色性视频 | 国产美女视频免费 | 亚洲一区二区麻豆 | 免费毛片一区二区三区久久久 | 九九热在线免费观看 | 久久日韩精品 | 色大片免费看 | 91九色精品| 久久国产精品久久精品国产演员表 | 国产午夜精品久久 | 久久久久国产精品午夜一区 | 综合色中文 | 在线观看一级 | 91av在线电影| 亚洲国产中文字幕 | 国产精品九九九九九九 | 成人国产精品一区 | 在线 影视 一区 | 国语自产偷拍精品视频偷 | 国产精品久久嫩一区二区免费 | 成人av免费看 | 欧美精品亚洲精品日韩精品 | 欧美韩国日本在线观看 | www..com黄色片| 久久综合狠狠综合久久狠狠色综合 | 日韩特级黄色片 | 狠狠干 狠狠操 | 久久久久亚洲精品男人的天堂 | 91精选在线| 欧美视频在线二区 | 国产免费又爽又刺激在线观看 | 国产91精品欧美 | 久久午夜精品视频 | 亚洲国产中文在线观看 | 黄色小说18 | 天天射天天添 | 成人禁用看黄a在线 | 国产亚洲久久 | 热久精品 | 国产视频精选 | 国产一线二线三线在线观看 | 激情综合色综合久久 | 国产精品成人久久久久 | 欧美精品国产综合久久 | 最近中文字幕免费 | 国产视频一二区 | 国产视频首页 | 91完整版观看 | 91精品久久久久久久91蜜桃 | 久久久久国产一区二区三区 | 看av免费 | 高清av免费观看 | 日韩免费一级a毛片在线播放一级 | 精品国产免费av | 精品国产欧美一区二区 | 亚洲欧美视频一区二区三区 | 五月天久久久久久 | 国产精品热| 五月天com| 欧美二区三区91 | 成人毛片一区 | 高潮久久久 | 丁五月婷婷 | 久久新视频 | 亚洲热久久 | 精品国产观看 | 婷婷爱五月天 | 99国产精品久久久久久久久久 | 中文字幕在线观看91 | 夜夜天天干 | av在线免费不卡 | 天堂av色婷婷一区二区三区 | 玖玖视频免费在线 | 粉嫩av一区二区三区入口 | 黄色在线免费观看网站 | 91桃色免费观看 | 午夜国产福利在线 | 日本婷婷色 | 成人动漫一区二区 | 高潮毛片无遮挡高清免费 | 亚洲综合小说 | 激情婷婷综合网 | www色,com | 精品久久久久久久久久久久久久久久 | 婷婷伊人五月 | 在线导航福利 | 奇米网8888 | 日韩电影在线一区二区 | 久久av免费 | 蜜桃av人人夜夜澡人人爽 | 日韩a级免费视频 | 久久av在线 | 探花系列在线 | 天天操天天舔天天爽 | 99久久99久久精品免费 | 一区二区三区免费 | 手机av片| 波多野结衣最新 | 免费看的国产视频网站 | a级国产乱理论片在线观看 伊人宗合网 | 亚洲 综合 国产 精品 | 在线看片中文字幕 | 日韩欧美视频在线 | 日韩肉感妇bbwbbwbbw | 精品美女在线观看 | 超碰免费久久 | 欧美日韩视频观看 | 91看国产| 国产高清久久久 | 欧美一区二区三区在线 | 欧洲精品一区二区 | 午夜影视剧场 | 欧美日韩国产色综合一二三四 | 欧美日韩伦理在线 | 国产精品69av | 国产高清久久 | 国产精品免费观看久久 | 久久亚洲免费 | 欧美成人黄 | 国产成人精品999在线观看 | 日韩免费在线观看视频 | 手机av在线免费观看 | 成人午夜电影免费在线观看 | 四虎国产精品免费观看视频优播 | 99久久精品国产亚洲 | 成人国产网站 | 亚洲另类交 | japanesexxxxfreehd乱熟 | 国产中文字幕在线视频 | 久久久福利影院 | 国产精品欧美激情在线观看 | 亚洲精品婷婷 | 黄色日本片| 国产美女精品久久久 | 国产精品一区二区av麻豆 | 久久久高清一区二区三区 | 狠狠狠狠狠狠狠干 | 国产.精品.日韩.另类.中文.在线.播放 | 99视频精品全国免费 | 免费精品在线视频 | www.av在线播放 | 91九色在线观看 | 亚洲精品国产第一综合99久久 | 最近日本韩国中文字幕 | 日韩精品一区二区在线 | 激情在线网站 | 蜜臀aⅴ国产精品久久久国产 | 久久国产精品影视 | 天天插天天射 | 国产中文字幕视频在线 | 91在线最新 | 丁香六月婷婷开心婷婷网 | 欧洲av不卡 | 欧美一级黄色视屏 | 久久久久综合 | 亚洲激情六月 | 亚洲成av人电影 | 狠狠躁18三区二区一区ai明星 | se婷婷| 久久久久国产精品免费免费搜索 | 国产午夜精品视频 | 99久久久成人国产精品 | 亚洲h在线播放在线观看h | 国产伦精品一区二区三区免费 | 色婷婷国产 | 免费在线国产精品 | 久久精品免费 | 日韩理论片在线观看 | 日韩电影精品 | 中文字幕第一 | 波多野结衣亚洲一区二区 | 日韩69av | 伊人国产在线播放 | 日韩电影中文字幕在线观看 | 精品国内自产拍在线观看视频 | 精品中文字幕在线观看 | 免费在线一区二区三区 | 97操碰| 狠狠躁日日躁夜夜躁av | 成人黄大片| 亚洲国产字幕 | 亚洲情感电影大片 | www五月天婷婷 | 日韩网站免费观看 | 狠狠干在线 | 黄色一区三区 | 成人91在线 | 夜夜爽88888免费视频4848 | 婷婷视频导航 | 成年人黄色在线观看 | 日韩簧片在线观看 | 国产96在线 | 最近日本中文字幕 | 国产视 | 国产伦精品一区二区三区免费 | 日韩在线中文字幕视频 | 二区三区在线视频 | 91麻豆精品国产91久久久使用方法 | 欧美精品久久久久性色 | 丁香婷婷深情五月亚洲 | 国产精品一区二区久久国产 | 日韩精品一区二区三区免费观看视频 | 亚洲激情五月 | www天天干com | 久久久久国产成人免费精品免费 | av成人黄色| 久久久久亚洲精品中文字幕 | 国产精品久久精品 | 97在线精品 | 亚洲精品久久激情国产片 | 午夜av一区 | 亚洲最新视频在线 | 久久久伊人网 | av在线播放亚洲 | 国产亚洲视频系列 | 欧美精品久久99 | 免费久久久久久 | 久热色超碰 | 久久av免费电影 | 久草在线观看视频免费 | av在线免费网 | 精品久久91 | 天天艹天天操 | 久久久久成人精品免费播放动漫 | 国产日韩中文字幕在线 | 999久久久久久久久久久 | 四虎欧美| 四虎在线免费观看 | 香蕉影视在线观看 | 国产精品欧美一区二区 | 久久精品中文字幕少妇 | 麻豆国产精品一区二区三区 | 色94色欧美| 91九色成人| 人人干人人草 | 国内精品久久久久久久影视简单 | 日本黄色免费电影网站 | 精品视频999 | 亚洲狠狠丁香婷婷综合久久久 | 国产午夜一区二区 | 久久视频在线观看免费 | 超碰人人干人人 | 国产精品久久久久久欧美 | 久久久亚洲麻豆日韩精品一区三区 | 免费在线观看av网址 | 日韩中文幕 |