來源| OSCHINA 社區(qū)
作者 |華為云開發(fā)者聯(lián)盟
摘要:使用動態(tài) SQL 并非一件易事,但借助可用于任何 SQL 映射語句中的強大的動態(tài) SQL 語言,MyBatis 顯著地提升了這一特性的易用性。
本文分享自華為云社區(qū)《MyBatis 詳解 - 動態(tài) SQL 使用與原理》,作者:龍哥手記 。 動態(tài) SQL 是 MyBatis 的強大特性之一。如果你使用過 JDBC 或其它類似的框架,你應該能理解根據(jù)不同條件拼接 SQL 語句有多痛苦,例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最后一個列名的逗號。利用動態(tài) SQL,可以徹底擺脫這種痛苦。 使用動態(tài) SQL 并非一件易事,但借助可用于任何 SQL 映射語句中的強大的動態(tài) SQL 語言,MyBatis 顯著地提升了這一特性的易用性。 如果你之前用過 JSTL 或任何基于類 XML 語言的文本處理器,你對動態(tài) SQL 元素可能會感覺似曾相識。在 MyBatis 之前的版本中,需要花時間了解大量的元素。借助功能強大的基于 OGNL 的表達式,MyBatis 3 替換了之前的大部分元素,大大精簡了元素種類,現(xiàn)在要學習的元素種類比原來的一半還要少。
if
choose (when, otherwise)
trim (where, set)
if
使用動態(tài) SQL 最常見情景是根據(jù)條件包含 where 子句的一部分。比如:
這條語句提供了可選的查找文本功能。如果不傳入 “title”,那么所有處于 “ACTIVE” 狀態(tài)的 BLOG 都會返回;如果傳入了 “title” 參數(shù),那么就會對 “title” 一列進行模糊查找并返回對應的 BLOG 結(jié)果(細心的讀者可能會發(fā)現(xiàn),“title” 的參數(shù)值需要包含查找掩碼或通配符字符)。
如果希望通過 “title” 和 “author” 兩個參數(shù)進行可選搜索該怎么辦呢?首先,我想先將語句名稱修改成更名副其實的名稱;接下來,只需要加入另一個條件即可。
choose、when、otherwise
有時候,我們不想使用所有的條件,而只是想從多個條件中選擇一個使用。針對這種情況,MyBatis 提供了 choose 元素,它有點像 Java 中的 switch 語句。
還是上面的例子,但是策略變?yōu)椋簜魅肓?“title” 就按 “title” 查找,傳入了 “author” 就按 “author” 查找的情形。若兩者都沒有傳入,就返回標記為 featured 的 BLOG(這可能是管理員認為,與其返回大量的無意義隨機 Blog,還不如返回一些由管理員挑選的 Blog)。
trim、where、set
前面幾個例子已經(jīng)合宜地解決了一個臭名昭著的動態(tài) SQL 問題。現(xiàn)在回到之前的 “if” 示例,這次我們將 “state = ‘ACTIVE’” 設置成動態(tài)條件,看看會發(fā)生什么。
如果沒有匹配的條件會怎么樣?最終這條 SQL 會變成這樣:
SELECT * FROM BLOG WHERE
這會導致查詢失敗。如果匹配的只是第二個條件又會怎樣?這條 SQL 會是這樣:
SELECT * FROM BLOG WHERE AND title like ‘someTitle’
這個查詢也會失敗。這個問題不能簡單地用條件元素來解決。這個問題是如此的難以解決,以至于解決過的人不會再想碰到這種問題。
MyBatis 有一個簡單且適合大多數(shù)場景的解決辦法。而在其他場景中,可以對其進行自定義以符合需求。而這,只需要一處簡單的改動:
where 元素只會在子元素返回任何內(nèi)容的情況下才插入 “WHERE” 子句。而且,若子句的開頭為 “AND” 或 “OR”,where 元素也會將它們?nèi)コ?/p>
如果 where 元素與你期望的不太一樣,你也可以通過自定義 trim 元素來定制 where 元素的功能。比如,和 where 元素等價的自定義 trim 元素為:
...
prefixOverrides 屬性會忽略通過管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子會移除所有 prefixOverrides 屬性中指定的內(nèi)容,并且插入 prefix 屬性中指定的內(nèi)容。
用于動態(tài)更新語句的類似解決方案叫做 set。set 元素可以用于動態(tài)包含需要更新的列,忽略其它不更新的列。比如:
update Author where id=#{id} username=#{username}, password=#{password}, email=#{email}, bio=#{bio}
這個例子中,set 元素會動態(tài)地在行首插入 SET 關(guān)鍵字,并會刪掉額外的逗號(這些逗號是在使用條件語句給列賦值時引入的)。
來看看與 set 元素等價的自定義 trim 元素吧:
...
注意,我們覆蓋了后綴值設置,并且自定義了前綴值。
foreach
動態(tài) SQL 的另一個常見使用場景是對集合進行遍歷(尤其是在構(gòu)建 IN 條件語句的時候)。比如:
foreach 元素的功能非常強大,它允許你指定一個集合,聲明可以在元素體內(nèi)使用的集合項(item)和索引(index)變量。它也允許你指定開頭與結(jié)尾的字符串以及集合項迭代之間的分隔符。這個元素也不會錯誤地添加多余的分隔符,看它多智能!
提示:你可以將任何可迭代對象(如 List、Set 等)、Map 對象或者數(shù)組對象作為集合參數(shù)傳遞給 foreach。當使用可迭代對象或者數(shù)組時,index 是當前迭代的序號,item 的值是本次迭代獲取到的元素。當使用 Map 對象(或者 Map.Entry 對象的集合)時,index 是鍵,item 是值。
至此,我們已經(jīng)完成了與 XML 配置及映射文件相關(guān)的討論。下一章將詳細探討 Java API,以便你能充分利用已經(jīng)創(chuàng)建的映射配置。
script
要在帶注解的映射器接口類中使用動態(tài) SQL,可以使用 script 元素。比如:
@Update({""}) void updateAuthorValues(Author author);
bind
bind 元素允許你在 OGNL 表達式以外創(chuàng)建一個變量,并將其綁定到當前的上下文。比如:
多數(shù)據(jù)庫支持
如果配置了 databaseIdProvider,你就可以在動態(tài)代碼中使用名為 “_databaseId” 的變量來為不同的數(shù)據(jù)庫構(gòu)建特定的語句。比如下面的例子:
insert into users values (#{id}, #{name}) select seq_users.nextval from dual select nextval for seq_users from sysibm.sysdummy1"
動態(tài) SQL 中的插入腳本語言
MyBatis 從 3.2 版本開始支持插入腳本語言,這允許你插入一種語言驅(qū)動,并基于這種語言來編寫動態(tài) SQL 查詢語句。
可以通過實現(xiàn)以下接口來插入一種語言:
public interface LanguageDriver { ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql); SqlSource createSqlSource(Configuration configuration, XNode script, Class> parameterType); SqlSource createSqlSource(Configuration configuration, String script, Class> parameterType); }
實現(xiàn)自定義語言驅(qū)動后,你就可以在 mybatis-config.xml 文件中將它設置為默認語言:
或者,你也可以使用 lang 屬性為特定的語句指定語言:
或者,在你的 mapper 接口上添加@Lang 注解:
public interface Mapper { @Lang(MyLanguageDriver.class) @Select("SELECT * FROM BLOG") ListselectBlog(); }
提示:可以使用 Apache Velocity 作為動態(tài)語言,更多細節(jié)請參考 MyBatis-Velocity 項目。
你前面看到的所有 xml 標簽都由默認 MyBatis 語言提供,而它由語言驅(qū)動 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver(別名為 xml)所提供。
動態(tài) SQL 解析原理
我們在使用 mybatis 的時候,會在 xml 中編寫 sql 語句。比如這段動態(tài) sql 代碼:
UPDATE users where id = ${id} name = #{name} , age = #{age} , birthday = #{birthday}
mybatis 底層是如何構(gòu)造這段 sql 的?下面帶著這個疑問,我們一步一步分析。
關(guān)于動態(tài) SQL 的接口和類
SqlNode 接口,簡單理解就是 xml 中的每個標簽,比如上述 sql 的 update,trim,if 標簽:
public interface SqlNode { boolean apply(DynamicContext context); }
SqlSource Sql 源接口,代表從 xml 文件或注解映射的 sql 內(nèi)容,主要就是用于創(chuàng)建 BoundSql,有實現(xiàn)類 DynamicSqlSource (動態(tài) Sql 源),StaticSqlSource (靜態(tài) Sql 源) 等:
public interface SqlSource { BoundSql getBoundSql(Object parameterObject); }
BoundSql 類,封裝 mybatis 最終產(chǎn)生 sql 的類,包括 sql 語句,參數(shù),參數(shù)源數(shù)據(jù)等參數(shù):
XNode,一個 Dom API 中的 Node 接口的擴展類:
BaseBuilder 接口及其實現(xiàn)類 (屬性,方法省略了,大家有興趣的自己看), 這些 Builder 的作用就是用于構(gòu)造 sql:
下面我們簡單分析下其中 4 個 Builder:
XMLConfigBuilder:解析 mybatis 中 configLocation 屬性中的全局 xml 文件,內(nèi)部會使用 XMLMapperBuilder 解析各個 xml 文件。
XMLMapperBuilder:遍歷 mybatis 中 mapperLocations 屬性中的 xml 文件中每個節(jié)點的 Builder,比如 user.xml,內(nèi)部會使用 XMLStatementBuilder 處理 xml 中的每個節(jié)點。
XMLStatementBuilder:解析 xml 文件中各個節(jié)點,比如 select,insert,update,delete 節(jié)點,內(nèi)部會使用 XMLScriptBuilder 處理節(jié)點的 sql 部分,遍歷產(chǎn)生的數(shù)據(jù)會丟到 Configuration 的 mappedStatements 中。
XMLScriptBuilder:解析 xml 中各個節(jié)點 sql 部分的 Builder。
LanguageDriver 接口及其實現(xiàn)類 (屬性,方法省略了,大家有興趣的自己看),該接口主要的作用就是構(gòu)造 sql:
簡單分析下 XMLLanguageDriver (處理 xml 中的 sql,RawLanguageDriver 處理靜態(tài) sql):XMLLanguageDriver 內(nèi)部會使用 XMLScriptBuilder 解析 xml 中的 sql 部分。
源碼分析走起
Spring 與 Mybatis 整合的時候需要配置 SqlSessionFactoryBean,該配置會加入數(shù)據(jù)源和 mybatis xml 配置文件路徑等信息:
我們就分析這一段配置背后的細節(jié):
SqlSessionFactoryBean 實現(xiàn)了 Spring 的 InitializingBean 接口,InitializingBean 接口的 afterPropertiesSet 方法中會調(diào)用 buildSqlSessionFactory 方法 該方法內(nèi)部會使用 XMLConfigBuilder 解析屬性 configLocation 中配置的路徑,還會使用 XMLMapperBuilder 屬性解析 mapperLocations 屬性中的各個 xml 文件。部分源碼如下:
由于 XMLConfigBuilder 內(nèi)部也是使用 XMLMapperBuilder,我們就看看 XMLMapperBuilder 的解析細節(jié):
我們關(guān)注一下,增刪改查節(jié)點的解析:
XMLStatementBuilder 的解析:
默認會使用 XMLLanguageDriver 創(chuàng)建 SqlSource(Configuration 構(gòu)造函數(shù)中設置)。
XMLLanguageDriver 創(chuàng)建 SqlSource:
XMLScriptBuilder 解析 sql:
得到 SqlSource 之后,會放到 Configuration 中,有了 SqlSource,就能拿 BoundSql 了,BoundSql 可以得到最終的 sql。
實例分析
以下面的 xml 解析大概說下 parseDynamicTags 的解析過程:
UPDATE users where id = ${id} name = #{name} , age = #{age} , birthday = #{birthday}
parseDynamicTags 方法的返回值是一個 List,也就是一個 Sql 節(jié)點集合。SqlNode 本文一開始已經(jīng)介紹,分析完解析過程之后會說一下各個 SqlNode 類型的作用。
首先根據(jù) update 節(jié)點 (Node) 得到所有的子節(jié)點,分別是 3 個子節(jié)點:
文本節(jié)點 UPDATE users
trim 子節(jié)點 ...
文本節(jié)點 where id = #
遍歷各個子節(jié)點:
如果節(jié)點類型是文本或者 CDATA,構(gòu)造一個 TextSqlNode 或 StaticTextSqlNode;
如果節(jié)點類型是元素,說明該 update 節(jié)點是個動態(tài) sql,然后會使用 NodeHandler 處理各個類型的子節(jié)點。這里的 NodeHandler 是 XMLScriptBuilder 的一個內(nèi)部接口,其實現(xiàn)類包括 TrimHandler、WhereHandler、SetHandler、IfHandler、ChooseHandler 等。看類名也就明白了這個 Handler 的作用,比如我們分析的 trim 節(jié)點,對應的是 TrimHandler;if 節(jié)點,對應的是 IfHandler... 這里子節(jié)點 trim 被 TrimHandler 處理,TrimHandler 內(nèi)部也使用 parseDynamicTags 方法解析節(jié)點。
遇到子節(jié)點是元素的話,重復以上步驟:
trim 子節(jié)點內(nèi)部有 7 個子節(jié)點,分別是文本節(jié)點、if 節(jié)點、是文本節(jié)點、if 節(jié)點、是文本節(jié)點、if 節(jié)點、文本節(jié)點。文本節(jié)點跟之前一樣處理,if 節(jié)點使用 IfHandler 處理。遍歷步驟如上所示,下面我們看下幾個 Handler 的實現(xiàn)細節(jié)。
IfHandler 處理方法也是使用 parseDynamicTags 方法,然后加上 if 標簽必要的屬性:
private class IfHandler implements NodeHandler { public void handleNode(XNode nodeToHandle, ListtargetContents) { List contents = parseDynamicTags(nodeToHandle); MixedSqlNode mixedSqlNode = new MixedSqlNode(contents); String test = nodeToHandle.getStringAttribute("test"); IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test); targetContents.add(ifSqlNode); } }
TrimHandler 處理方法也是使用 parseDynamicTags 方法,然后加上 trim 標簽必要的屬性:
private class TrimHandler implements NodeHandler { public void handleNode(XNode nodeToHandle, ListtargetContents) { List contents = parseDynamicTags(nodeToHandle); MixedSqlNode mixedSqlNode = new MixedSqlNode(contents); String prefix = nodeToHandle.getStringAttribute("prefix"); String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides"); String suffix = nodeToHandle.getStringAttribute("suffix"); String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides"); TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides); targetContents.add(trim); } }
以上 update 方法最終通過 parseDynamicTags 方法得到的 SqlNode 集合如下:
trim 節(jié)點:
由于這個 update 方法是個動態(tài)節(jié)點,因此構(gòu)造出了 DynamicSqlSource。DynamicSqlSource 內(nèi)部就可以構(gòu)造 sql 了:
DynamicSqlSource 內(nèi)部的 SqlNode 屬性是一個 MixedSqlNode。然后我們看看各個 SqlNode 實現(xiàn)類的 apply 方法。下面分析一下各個 SqlNode 實現(xiàn)類的 apply 方法實現(xiàn):
MixedSqlNode:MixedSqlNode 會遍歷調(diào)用內(nèi)部各個 sqlNode 的 apply 方法。
public boolean apply(DynamicContext context) { for (SqlNode sqlNode : contents) { sqlNode.apply(context); } return true; }
StaticTextSqlNode:直接 append sql 文本。
public boolean apply(DynamicContext context) { context.appendSql(text); return true; }
IfSqlNode:這里的 evaluator 是一個 ExpressionEvaluator 類型的實例,內(nèi)部使用了 OGNL 處理表達式邏輯。
public boolean apply(DynamicContext context) { if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; }
TrimSqlNode:
public boolean apply(DynamicContext context) { FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); boolean result = contents.apply(filteredDynamicContext); filteredDynamicContext.applyAll(); return result; } public void applyAll() { sqlBuffer = new StringBuilder(sqlBuffer.toString().trim()); String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH); if (trimmedUppercaseSql.length() > 0) { applyPrefix(sqlBuffer, trimmedUppercaseSql); applySuffix(sqlBuffer, trimmedUppercaseSql); } delegate.appendSql(sqlBuffer.toString()); } private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { if (!prefixApplied) { prefixApplied = true; if (prefixesToOverride != null) { for (String toRemove : prefixesToOverride) { if (trimmedUppercaseSql.startsWith(toRemove)) { sql.delete(0, toRemove.trim().length()); break; } } } if (prefix != null) { sql.insert(0, " "); sql.insert(0, prefix); } } }
TrimSqlNode 的 apply 方法也是調(diào)用屬性 contents (一般都是 MixedSqlNode) 的 apply 方法,按照實例也就是 7 個 SqlNode,都是 StaticTextSqlNode 和 IfSqlNode。最后會使用 FilteredDynamicContext 過濾掉 prefix 和 suffix。
審核編輯:湯梓紅
-
處理器
+關(guān)注
關(guān)注
68文章
19432瀏覽量
231254 -
SQL
+關(guān)注
關(guān)注
1文章
775瀏覽量
44262 -
OGNL
+關(guān)注
關(guān)注
0文章
2瀏覽量
1968 -
mybatis
+關(guān)注
關(guān)注
0文章
62瀏覽量
6745
原文標題:一文掌握 MyBatis 的動態(tài) SQL 使用與原理
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
一文了解MyBatis的查詢原理
jdbc與mybatis的區(qū)別
![jdbc與<b class='flag-5'>mybatis</b>的區(qū)別](https://file.elecfans.com/web1/M00/45/B7/o4YBAFp0MrCAG9C_AAF7WGjLqX8166.png)
評論