參考:
https://www.cnblogs.com/ysocean/p/7289529.html
https://www.cnblogs.com/fangjian0423/p/mybaits-dynamic-sql-analysis.html
mybatis 詳解(五)------動態SQL
前面幾篇博客我們通過實例講解了用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 where username=?;
如果 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解析的結果是一樣的,非常迫切地想知道這到底是為什么,然后這也促使了我去看源碼的決心。最終還是看下來了。