前言
今天的內容是關於昨天優化的mybatis
動態sql
的一次簡單總結,簡單來說就是我通過trim
實現了不確定參數union all
的可變查詢,讓之前的動態sql
邏輯更加簡潔,內容當然算不上高大上,只能算是給可能遇到問題的小伙伴探個路,下面我們就來展開看下吧。
背景
最近開發的一個功能要用到用戶中心的一個接口,原有接口無法滿足我的需求,所以我需要自己擴展一個新的接口,這個接口的需要實現的功能也很簡單,就是根據崗位id
、用戶id
或者用戶組id
獲取一批用戶信息。
由於接口涉及到多個表的組合查詢,包括用戶信息表、崗位用戶映射信息表、用戶用戶組映射信息表等,而且參數是可以為空的(至少有一個參數不為空,否則也不會調用接口),所以在實現的時候我就有考慮到多個查詢通過union all
來拼接。
但是由於參數可能為空,所以union all
是通過動態拼接的,最開始我是通過if
判斷進行拼接的,剛開始接口一直都沒有問題,但是昨天測試同學在測試的時候,發現如果單傳用戶組id
的話,接口會報錯,然后我就開始對這個接口的sql
進行了優化,剛開始我是這么寫的:
<select id="listUsersInfoIds" resultType="io.github.syske.user.UserInfo">
select ui.id,
ui.userId,
ui.name,
ui.active
from (
<if test="userIds != null and userIds.size > 0">
select
u.id,
u.user_id as userId,
u.name,
u.active
from user u
where u.id in
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId, jdbcType=BIGINT}
</foreach>
and u.active = true
</if>
<if test="postIds != null and postIds.size > 0">
<if test="userIds != null and and userIds.size > 0">
union all
</if>
select
u.id,
u.user_id as userId,
u.name,
u.active
from post_user_mapping m,
user u
where m.post_id in
<foreach collection="postIds" item="postId" open="(" close=")" separator=",">
#{postId, jdbcType=BIGINT}
</foreach>
and m.user_id=u.id
and u.active = true
</if>
<if test="groupIds != null and groupIds.size > 0">
<if test="(postIds != null and postIds.size > 0) or (userIds != null and userIds.size > 0)">
union all
</if>
select
u.id,
u.user_id as userId,
u.name,
u.active
from group_user_mapping m,
user u
where m.group_id in
<foreach collection="groupIds" item="groupId" open="(" close=")" separator=",">
#{groupId, jdbcType=BIGINT}
</foreach>
and m.user_id=u.id
and u.active = true
</if>
) ui group by ui.id
</select>
但是上面的寫法在只傳groupIds
的時候會報錯,准確來說是groupIds
這里拼接union all
的語句會報錯,應該是不支持or
這種復雜語句的,之后我把這里的if
條件語句改成這樣:
<if test="(postIds != null and postIds.size > 0) and (userIds == null or userIds.size == 0)">
union all
</if>
<if test="(postIds == null or postIds.size == 0) and (userIds != null and userIds.size > 0)">
union all
</if>
也就是分別判斷postIds
和userIds
是不是有一個一個不為空,如果是則拼接union all
,當然最后我測試了下發現確實解決了,但是我覺得這種方式不夠優雅,而且不夠靈活,特別是如果我后面還需要加入union all
語句的時候,那就要再多判斷一個字段,越往后需要判斷的字段就越多,然后我再網上找了一圈並沒有找到解決方法,最后我打算看下mybatis
的文檔,幸運的是我還真找到了自己想要的答案。
解決方案
今天的解決方案是基於trim
標簽實現的,所以下面我們先來看下trim
的一些知識點。
trim標簽
在我們大多數的需求場景下,mybatis
提供的動態語句語法已經可以勝任了,比如if
、where
、choose
、when
、otherwise
、foreach
,再復雜一點的還有set
,但是像我現在的需求他們都沒辦法完美解決(畢竟用if
太過繁瑣),於是我發現了一個靈活性更高的標簽——trim
。
簡單探索
trim
標簽的作用就是幫助我們生成更復雜的sql
,關於它的具體作用官方文檔並沒有給出明確說明,但是根據它的幾個參數以及示例,我們可以看出它的用法。我們先看下trim
標簽的幾個屬性:
suffixOverrides
:要替換的后綴(被替換內容)suffix
:替換的后綴(替換內容)prefixOverrides
:要替換的前綴(被替換內容)prefix
:替換的前綴(替換內容)
但看這四個屬性確實可能有點迷,下面我們通過幾個實例來說明下trim
的用法。
前置用法
先看第一個,也是官方給出的示例——通過trim
來實現where
標簽,用where
標簽我們通常是這么寫的:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
OR author_name like #{author.name}
</if>
</where>
</select>
用trim
實現的話,可以這樣寫:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<trim prefix="where" prefixOverrides="AND | OR">
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
OR author_name like #{author.name}
</if>
</trim>
</select>
這里trim
標簽的意思就是把trim
標簽中第一個AND
或者OR
替換為where
,也就是說如果第一個條件為空,第二個條件中的AND
會被替換成where
,如果前兩個條件都為空,第三個條件中的OR
會被替換為where
。
后置用法
上面我們演示了前置替換的用法,下面我們來看下后置用法,后置用法是通過trim
來實現set
標簽(話說我之前好像也用的不多,孤陋寡聞了),通常情況下的set
是這么用的:
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
set
標簽的作用就是當如上語句中,第四個更新語句為空的時候,會將set
標簽內末尾的,
移除掉,並在標簽內語句開始出加上set
關鍵字。用trim
標簽的話,可以這么寫:
<update id="updateAuthorIfNecessary">
update Author
<trim prefix="set" suffixOverrides=','>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</trim>
where id=#{id}
</update>
好了,關於trim
我們就演示這么多,下面我們做一個簡單總結:
prefix
:表示前置要插入的內容(這樣看,前面說的替換有點不太合理),比如where
、set
,它可以單獨使用suffix
:表示后置插入的內容(同prefix
)prefixOverrides
:表示前置要移除的內容(中文翻譯前置覆寫)suffixOverrides
:表示后置要移除的內容(同prefixOverrides
)
也就是說trim
本質上就是通過這四個屬性,實現在語句前后加上或者移除相關內容,來實現復雜的動態sql
,在實現方面也很簡單,但是靈活度更多。
解決我的問題
最后讓我們再回到我前面說的優化,我的這個sql
如果用trim
實現的話,可以這樣寫:
<select id="listUsersInfoIds" resultType="net.coolcollege.user.facade.model.user.UserInfo">
select ui.id,
ui.userId,
ui.name,
ui.active
from (
<trim suffixOverrides="union all">
<trim suffix="union all">
<if test="userIds != null and userIds.size > 0">
select
u.id,
u.user_id as userId,
u.name,
u.active
from user u
where u.id in
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId, jdbcType=BIGINT}
</foreach>
and u.active = true
</if>
</trim>
<trim suffix="union all">
<if test="postIds != null and postIds.size > 0">
select
u.id,
u.user_id as userId,
u.name,
u.active
from post_user_mapping m,
user u
where m.post_id in
<foreach collection="postIds" item="postId" open="(" close=")" separator=",">
#{postId, jdbcType=BIGINT}
</foreach>
and m.user_id=u.id
and u.active = true
</if>
</trim>
<if test="groupIds != null and groupIds.size > 0">
select
u.id,
u.user_id as userId,
u.name,
u.active
from group_user_mapping m,
user u
where m.group_id in
<foreach collection="groupIds" item="groupId" open="(" close=")" separator=",">
#{groupId, jdbcType=BIGINT}
</foreach>
and m.user_id=u.id
and u.active = true
</if>
</trim>
) ui group by ui.id
</select>
首先我通過一個大的trim
包裝所有子查詢(之前通過union all
連接),條件是移除最后的union all
,然后再用一個trim
標簽包裝除最后一個子查詢之外的其他子查詢,條件是再語句末尾加上union all
,這樣前面需要通過復雜if
判斷的語句就直接省略了,而且好處也很明顯:
后續不論我增加多少個子查詢,我只需要給子查詢加上trim
標簽即可(條件都一樣),而不需要關心其他子查詢是否為空,這樣整個sql
不僅更簡潔,而且擴展性也很強,后期不論我增加多少個子查詢,只需要給子查詢加上trim
標簽即可,而不需要處理其他復雜判斷。
結語
mybatis
算是一個比較流行的ORM
框架,應該說是國內最主流的數據庫交互框架了,但是從我自身使用的情況來說,大多數復雜場景我好像只想到了if
、choose
、when
、where
、foreach
等,甚至連set
都沒用過,這樣不僅導致寫出的動態sql
邏輯復雜,不夠簡潔,不利於后期維護,而且很容易出錯。
總之,我是覺得學習東西,我們不應該僅僅停留在夠用和滿足需求的程度,而應該養成多看官方文檔、多探索的習慣,選擇更適合、更優的解決方案,這樣才不至於成為井底之蛙。好了,今天的內容就到這里吧!