MyBatis-動態SQL使用和原理
參考:
https://www.cnblogs.com/ysocean/p/7289529.html
https://www.cnblogs.com/fangjian0423/p/mybaits-dynamic-sql-analysis.html
mybatis 詳解(五)------動態SQL
目錄
- 1、動態SQL:if 語句
- 2、動態SQL:if+where語句
- 3、動態SQL:if+set 語句
- 4、動態SQL:choose(when,otherwise) 語句
- 5、動態SQL:trim 語句
- 6、動態SQL: SQL 片段
- 7、動態SQL: foreach 語句
- 8、總結
前面幾篇部落格我們通過例項講解了用mybatis對一張表進行的CRUD操作,但是我們發現寫的 SQL 語句都比較簡單,如果有比較複雜的業務,我們需要寫複雜的 SQL 語句,往往需要拼接,而拼接 SQL ,稍微不注意,由於引號,空格等缺失可能都會導致錯誤。
那麼怎麼去解決這個問題呢?這就是本篇所講的使用 mybatis 動態SQL,通過 if, choose, when, otherwise, trim, where, set, foreach等標籤,可組合成非常靈活的SQL語句,從而在提高 SQL 語句的準確性的同時,也大大提高了開發人員的效率。
我們以 User 表為例來說明:
回到頂部
1、動態SQL:if 語句
根據 username 和 sex 來查詢資料。如果username為空,那麼將只根據sex來查詢;反之只根據username來查詢
首先不使用 動態SQL 來書寫
1 2 3 4 5 6 |
<select id= "selectUserByUsernameAndSex"
resultType= "user" parameterType= "com.ys.po.User" >
<!-- 這裡和普通的sql 查詢語句差不多,對於只有一個引數,後面的 #{id}表示佔位符,裡面不一定要寫id,
寫啥都可以,但是不要空著,如果有多個引數則必須寫pojo類裡面的屬性 -->
select * from user where username=#{username} and sex=#{sex}
</select>
|
上面的查詢語句,我們可以發現,如果 #{username} 為空,那麼查詢結果也是空,如何解決這個問題呢?使用 if 來判斷
1 2 3 4 5 6 7 8 9 10 |
<select id= "selectUserByUsernameAndSex" resultType= "user" parameterType= "com.ys.po.User" >
select * from user where
< if test= "username != null" >
username=#{username}
</ if >
< if test= "username != null" >
and sex=#{sex}
</ if >
</select>
|
這樣寫我們可以看到,如果 sex 等於 null,那麼查詢語句為 select * from user where username=#{username},但是如果usename 為空呢?那麼查詢語句為 select * from user where and sex=#{sex},這是錯誤的 SQL 語句,如何解決呢?請看下面的 where 語句
回到頂部2、動態SQL:if+where語句
1 2 3 4 5 6 7 8 9 10 11 12 |
<select id= "selectUserByUsernameAndSex" resultType= "user" parameterType= "com.ys.po.User" >
select * from user
<where>
< if test= "username != null" >
username=#{username}
</ if >
< if test= "username != null" >
and sex=#{sex}
</ if >
</where>
</select>
|
這個“where”標籤會知道如果它包含的標籤中有返回值的話,它就插入一個‘where’。此外,如果標籤返回的內容是以AND 或OR 開頭的,則它會剔除掉。
回到頂部
3、動態SQL:if+set 語句
同理,上面的對於查詢 SQL 語句包含 where 關鍵字,如果在進行更新操作的時候,含有 set 關鍵詞,我們怎麼處理呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!-- 根據 id 更新 user 表的資料 -->
<update id= "updateUserById" parameterType= "com.ys.po.User" >
update user u
<set>
< if test= "username != null and username != ''" >
u.username = #{username},
</ if >
< if test= "sex != null and sex != ''" >
u.sex = #{sex}
</ if >
</set>
where id=#{id}
</update>
|
這樣寫,如果第一個條件 username 為空,那麼 sql 語句為:update user u set u.sex=? where id=?
如果第一個條件不為空,那麼 sql 語句為:update user u set u.username = ? ,u.sex = ? where id=?
回到頂部4、動態SQL:choose(when,otherwise) 語句
有時候,我們不想用到所有的查詢條件,只想選擇其中的一個,查詢條件有一個滿足即可,使用 choose 標籤可以解決此類問題,類似於 Java 的 switch 語句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<select id= "selectUserByChoose" resultType= "com.ys.po.User" parameterType= "com.ys.po.User" >
select * from user
<where>
<choose>
<when test= "id !='' and id != null" >
id=#{id}
</when>
<when test= "username !='' and username != null" >
and username=#{username}
</when>
<otherwise>
and sex=#{sex}
</otherwise>
</choose>
</where>
</select>
|
也就是說,這裡我們有三個條件,id,username,sex,只能選擇一個作為查詢條件
如果 id 不為空,那麼查詢語句為:select * from user where id=?
如果 id 為空,那麼看username 是否為空,如果不為空,那麼語句為 select * from user whereusername=?;
如果 username 為空,那麼查詢語句為 select * from user where sex=?
回到頂部
5、動態SQL:trim 語句
trim標記是一個格式化的標記,可以完成set或者是where標記的功能
①、用 trim 改寫上面第二點的 if+where 語句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<select id= "selectUserByUsernameAndSex" resultType= "user" parameterType= "com.ys.po.User" >
select * from user
<!-- <where>
< if test= "username != null" >
username=#{username}
</ if >
< if test= "username != null" >
and sex=#{sex}
</ if >
</where> -->
<trim prefix= "where" prefixOverrides= "and | or" >
< if test= "username != null" >
and username=#{username}
</ if >
< if test= "sex != null" >
and sex=#{sex}
</ if >
</trim>
</select>
|
prefix:字首
prefixoverride:去掉第一個and或者是or
②、用 trim 改寫上面第三點的 if+set語句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!-- 根據 id 更新 user 表的資料 -->
<update id= "updateUserById" parameterType= "com.ys.po.User" >
update user u
<!-- <set>
< if test= "username != null and username != ''" >
u.username = #{username},
</ if >
< if test= "sex != null and sex != ''" >
u.sex = #{sex}
</ if >
</set> -->
<trim prefix= "set" suffixOverrides= "," >
< if test= "username != null and username != ''" >
u.username = #{username},
</ if >
< if test= "sex != null and sex != ''" >
u.sex = #{sex},
</ if >
</trim>
where id=#{id}
</update>
|
suffix:字尾
suffixoverride:去掉最後一個逗號(也可以是其他的標記,就像是上面字首中的and一樣)
回到頂部6、動態SQL: SQL 片段
有時候可能某個 sql 語句我們用的特別多,為了增加程式碼的重用性,簡化程式碼,我們需要將這些程式碼抽取出來,然後使用時直接呼叫。
比如:假如我們需要經常根據使用者名稱和性別來進行聯合查詢,那麼我們就把這個程式碼抽取出來,如下:
1 2 3 4 5 6 7 8 9 |
<!-- 定義 sql 片段 -->
<sql id= "selectUserByUserNameAndSexSQL" >
< if test= "username != null and username != ''" >
AND username = #{username}
</ if >
< if test= "sex != null and sex != ''" >
AND sex = #{sex}
</ if >
</sql>
|
引用 sql 片段
1 2 3 4 5 6 7 8 |
<select id= "selectUserByUsernameAndSex" resultType= "user" parameterType= "com.ys.po.User" >
select * from user
<trim prefix= "where" prefixOverrides= "and | or" >
<!-- 引用 sql 片段,如果refid 指定的不在本檔案中,那麼需要在前面加上 namespace -->
<include refid= "selectUserByUserNameAndSexSQL" ></include>
<!-- 在這裡還可以引用其他的 sql 片段 -->
</trim>
</select>
|
注意:①、最好基於 單表來定義 sql 片段,提高片段的可重用性
②、在 sql 片段中最好不要包括 where
回到頂部
7、動態SQL: foreach 語句
需求:我們需要查詢 user 表中 id 分別為1,2,3的使用者
sql語句:select * from user where id=1 or id=2 or id=3
select * from user where id in (1,2,3)
①、建立一個 UserVo 類,裡面封裝一個 List<Integer> ids 的屬性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.ys.vo;
import java.util.List;
public class UserVo {
//封裝多個使用者的id
private List<Integer> ids;
public List<Integer> getIds() {
return ids;
}
public void setIds(List<Integer> ids) {
this .ids = ids;
}
}
|
②、我們用 foreach 來改寫select * from user where id=1 or id=2 or id=3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<select id= "selectUserByListId" parameterType= "com.ys.vo.UserVo" resultType= "com.ys.po.User" >
select * from user
<where>
<!--
collection:指定輸入物件中的集合屬性
item:每次遍歷生成的物件
open:開始遍歷時的拼接字串
close:結束時拼接的字串
separator:遍歷物件之間需要拼接的字串
select * from user where 1 = 1 and (id= 1 or id= 2 or id= 3 )
-->
<foreach collection= "ids" item= "id" open= "and (" close= ")" separator= "or" >
id=#{id}
</foreach>
</where>
</select>
|
測試:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//根據id集合查詢user表資料
@Test
public void testSelectUserByListId(){
String statement = "com.ys.po.userMapper.selectUserByListId" ;
UserVo uv = new UserVo();
List<Integer> ids = new ArrayList<>();
ids.add( 1 );
ids.add( 2 );
ids.add( 3 );
uv.setIds(ids);
List<User> listUser = session.selectList(statement, uv);
for (User u : listUser){
System.out.println(u);
}
session.close();
}
|
③、我們用 foreach 來改寫select * from user where id in (1,2,3)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<select id= "selectUserByListId" parameterType= "com.ys.vo.UserVo" resultType= "com.ys.po.User" >
select * from user
<where>
<!--
collection:指定輸入物件中的集合屬性
item:每次遍歷生成的物件
open:開始遍歷時的拼接字串
close:結束時拼接的字串
separator:遍歷物件之間需要拼接的字串
select * from user where 1 = 1 and id in ( 1 , 2 , 3 )
-->
<foreach collection= "ids" item= "id" open= "and id in (" close= ") " separator= "," >
#{id}
</foreach>
</where>
</select>
|
8、總結
其實動態 sql 語句的編寫往往就是一個拼接的問題,為了保證拼接準確,我們最好首先要寫原生的 sql 語句出來,然後在通過 mybatis 動態sql 對照著改,防止出錯。
Mybatis解析動態sql原理分析
前言
廢話不多說,直接進入文章。
我們在使用mybatis的時候,會在xml中編寫sql語句。
比如這段動態sql程式碼:
<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
UPDATE users
<trim prefix="SET" prefixOverrides=",">
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="age != null and age != ''">
, age = #{age}
</if>
<if test="birthday != null and birthday != ''">
, birthday = #{birthday}
</if>
</trim>
where id = ${id}
</update>
mybatis底層是如何構造這段sql的?
這方面的知識網上資料不多,於是就寫了這麼一篇文章。
下面帶著這個疑問,我們一步一步分析。
介紹MyBatis中一些關於動態SQL的介面和類
SqlNode介面,簡單理解就是xml中的每個標籤,比如上述sql的update,trim,if標籤:
public interface SqlNode {
boolean apply(DynamicContext context);
}
SqlSource Sql源介面,代表從xml檔案或註解對映的sql內容,主要就是用於建立BoundSql,有實現類DynamicSqlSource(動態Sql源),StaticSqlSource(靜態Sql源)等:
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
BoundSql類,封裝mybatis最終產生sql的類,包括sql語句,引數,引數源資料等引數:
XNode,一個Dom API中的Node介面的擴充套件類。
BaseBuilder介面及其實現類(屬性,方法省略了,大家有興趣的自己看),這些Builder的作用就是用於構造sql:
下面我們簡單分析下其中4個Builder:
1 XMLConfigBuilder
解析mybatis中configLocation屬性中的全域性xml檔案,內部會使用XMLMapperBuilder解析各個xml檔案。
2 XMLMapperBuilder
遍歷mybatis中mapperLocations屬性中的xml檔案中每個節點的Builder,比如user.xml,內部會使用XMLStatementBuilder處理xml中的每個節點。
3 XMLStatementBuilder
解析xml檔案中各個節點,比如select,insert,update,delete節點,內部會使用XMLScriptBuilder處理節點的sql部分,遍歷產生的資料會丟到Configuration的mappedStatements中。
4 XMLScriptBuilder
解析xml中各個節點sql部分的Builder。
LanguageDriver介面及其實現類(屬性,方法省略了,大家有興趣的自己看),該介面主要的作用就是構造sql:
簡單分析下XMLLanguageDriver(處理xml中的sql,RawLanguageDriver處理靜態sql):
XMLLanguageDriver內部會使用XMLScriptBuilder解析xml中的sql部分。
ok, 大部分比較重要的類我們都已經介紹了,下面原始碼分析走起。
原始碼分析走起
Spring與Mybatis整合的時候需要配置SqlSessionFactoryBean,該配置會加入資料來源和mybatis xml配置檔案路徑等資訊:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatisConfig.xml"/>
<property name="mapperLocations" value="classpath*:org/format/dao/*.xml"/>
</bean>
我們就分析這一段配置背後的細節:
SqlSessionFactoryBean實現了Spring的InitializingBean介面,InitializingBean介面的afterPropertiesSet方法中會呼叫buildSqlSessionFactory方法
buildSqlSessionFactory方法內部會使用XMLConfigBuilder解析屬性configLocation中配置的路徑,還會使用XMLMapperBuilder屬性解析mapperLocations屬性中的各個xml檔案。
部分原始碼如下:
由於XMLConfigBuilder內部也是使用XMLMapperBuilder,我們就看看XMLMapperBuilder的解析細節。
我們關注一下,增刪改查節點的解析。
XMLStatementBuilder的解析:
預設會使用XMLLanguageDriver建立SqlSource(Configuration建構函式中設定)。
XMLLanguageDriver建立SqlSource:
XMLScriptBuilder解析sql:
得到SqlSource之後,會放到Configuration中,有了SqlSource,就能拿BoundSql了,BoundSql可以得到最終的sql。
例項分析
我以以下xml的解析大概說下parseDynamicTags的解析過程:
<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
UPDATE users
<trim prefix="SET" prefixOverrides=",">
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="age != null and age != ''">
, age = #{age}
</if>
<if test="birthday != null and birthday != ''">
, birthday = #{birthday}
</if>
</trim>
where id = ${id}
</update>
在看這段解析之前,請先了解dom相關的知識,xml dom知識,dom博文
parseDynamicTags方法的返回值是一個List,也就是一個Sql節點集合。SqlNode本文一開始已經介紹,分析完解析過程之後會說一下各個SqlNode型別的作用。
1 首先根據update節點(Node)得到所有的子節點,分別是3個子節點
(1)文字節點 \n UPDATE users
(2)trim子節點...
(3)文字節點 \n where id = #{id}
2 遍歷各個子節點
(1) 如果節點型別是文字或者CDATA,構造一個TextSqlNode或StaticTextSqlNode
(2) 如果節點型別是元素,說明該update節點是個動態sql,然後會使用NodeHandler處理各個型別的子節點。這裡的NodeHandler是XMLScriptBuilder的一個內部介面,其實現類包括TrimHandler、WhereHandler、SetHandler、IfHandler、ChooseHandler等。看類名也就明白了這個Handler的作用,比如我們分析的trim節點,對應的是TrimHandler;if節點,對應的是IfHandler...
這裡子節點trim被TrimHandler處理,TrimHandler內部也使用parseDynamicTags方法解析節點
3 遇到子節點是元素的話,重複以上步驟
trim子節點內部有7個子節點,分別是文字節點、if節點、是文字節點、if節點、是文字節點、if節點、文字節點。文字節點跟之前一樣處理,if節點使用IfHandler處理
遍歷步驟如上所示,下面我們看下幾個Handler的實現細節。
IfHandler處理方法也是使用parseDynamicTags方法,然後加上if標籤必要的屬性。
private class IfHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> 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, List<SqlNode> targetContents) {
List<SqlNode> 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節點:
由於這個update方法是個動態節點,因此構造出了DynamicSqlSource。
DynamicSqlSource內部就可以構造sql了:
DynamicSqlSource內部的SqlNode屬性是一個MixedSqlNode。
然後我們看看各個SqlNode實現類的apply方法
下面分析一下兩個SqlNode實現類的apply方法實現:
MixedSqlNode:
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
MixedSqlNode會遍歷呼叫內部各個sqlNode的apply方法。
StaticTextSqlNode:
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
直接append sql文字。
IfSqlNode:
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
這裡的evaluator是一個ExpressionEvaluator型別的例項,內部使用了OGNL處理表達式邏輯。
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方法也是呼叫屬性contents(一般都是MixedSqlNode)的apply方法,按照例項也就是7個SqlNode,都是StaticTextSqlNode和IfSqlNode。 最後會使用FilteredDynamicContext過濾掉prefix和suffix。
總結
大致講解了一下mybatis對動態sql語句的解析過程,其實回過頭來看看不算複雜,還算蠻簡單的。 之前接觸mybaits的時候遇到剛才分析的那一段動態sql的時候總是很費解。
<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
UPDATE users
<trim prefix="SET" prefixOverrides=",">
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="age != null and age != ''">
, age = #{age}
</if>
<if test="birthday != null and birthday != ''">
, birthday = #{birthday}
</if>
</trim>
where id = ${id}
</update>
想搞明白這個trim節點的prefixOverrides到底是什麼意思(從字面上理解就是字首覆蓋),而且官方文件上也沒這方面知識的說明。我將這段xml改成如下:
<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
UPDATE users
<trim prefix="SET" prefixOverrides=",">
<if test="name != null and name != ''">
, name = #{name}
</if>
<if test="age != null and age != ''">
, age = #{age}
</if>
<if test="birthday != null and birthday != ''">
, birthday = #{birthday}
</if>
</trim>
where id = ${id}
</update>
(第二段第一個if節點多了個逗號) 結果我發現這2段xml解析的結果是一樣的,非常迫切地想知道這到底是為什麼,然後這也促使了我去看原始碼的決心。最終還是看下來了。