記一次mybatis復雜動態sql拼接優化方案


前言

今天的內容是關於昨天優化的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>

也就是分別判斷postIdsuserIds是不是有一個一個不為空,如果是則拼接union all,當然最后我測試了下發現確實解決了,但是我覺得這種方式不夠優雅,而且不夠靈活,特別是如果我后面還需要加入union all語句的時候,那就要再多判斷一個字段,越往后需要判斷的字段就越多,然后我再網上找了一圈並沒有找到解決方法,最后我打算看下mybatis的文檔,幸運的是我還真找到了自己想要的答案。

解決方案

今天的解決方案是基於trim標簽實現的,所以下面我們先來看下trim的一些知識點。

trim標簽

在我們大多數的需求場景下,mybatis提供的動態語句語法已經可以勝任了,比如ifwherechoosewhenotherwiseforeach,再復雜一點的還有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:表示前置要插入的內容(這樣看,前面說的替換有點不太合理),比如whereset,它可以單獨使用
  • 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框架,應該說是國內最主流的數據庫交互框架了,但是從我自身使用的情況來說,大多數復雜場景我好像只想到了ifchoosewhenwhereforeach等,甚至連set都沒用過,這樣不僅導致寫出的動態sql邏輯復雜,不夠簡潔,不利於后期維護,而且很容易出錯。

總之,我是覺得學習東西,我們不應該僅僅停留在夠用和滿足需求的程度,而應該養成多看官方文檔、多探索的習慣,選擇更適合、更優的解決方案,這樣才不至於成為井底之蛙。好了,今天的內容就到這里吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM