【MyBatis學習總結 (五),動態SQL】

Moluuu發表於2020-10-26

在之前的四篇文章中,我們已經學了MyBatis不少東西,剩下能簡單學的也不多了, MyBatis的學習也漸漸接近尾聲。

以下是之前文章的地址

(MyBatis學習總結(四),註解 & 多對一、一對多)

(MyBatis學習總結(三),ResultMap & 日誌、分頁)

(MyBatis學習總結(二),MyBatis實現CURD & MyBatis常用配置涉及)

MyBatis學習總結(一),初步理解MyBatis & 簡單實現

動態SQL這個點,我們作為MyBatis學習的壓軸部分,寫在倒數第二篇文章中。

動態SQL也不是三言兩語能夠講清楚的,需要一定的篇幅,所以本篇文章只寫動態SQL。

動態SQL

動態 SQL 是 MyBatis 的強大特性之一。如果你使用過 JDBC 或其它類似的框架,你應該能理解根據不同條件拼接 SQL 語句有多痛苦,例如拼接時要確保不能忘記新增必要的空格,還要注意去掉列表最後一個列名的逗號。利用動態 SQL,可以徹底擺脫這種痛苦。

如果你之前用過 JSTL 或任何基於類 XML 語言的文字處理器,你對動態 SQL 元素可能會感覺似曾相識。在 MyBatis 之前的版本中,需要花時間瞭解大量的元素。藉助功能強大的基於 OGNL 的表示式,MyBatis 3 替換了之前的大部分元素,大大精簡了元素種類,現在要學習的元素種類比原來的一半還要少。

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

在涉及動態sql之前我們先搭建一下環境

搭建動態sql環境

建立表

create table `blog` (
	`id` varchar(50) not null comment '部落格id',
    `title` varchar(100) not null comment '部落格標題',
    `author` varchar(30) not null comment '作者',
    `createTime` datetime not null comment '建立時間',
    `views` int(30) not null comment '瀏覽量' 
) engine=InnoDB default charset=utf8;

建立完表後,我們不再使用sqlyog來直接插入資料,折騰一下使用Java程式碼來插入資料

編寫實體類

public class Blog {
    private String id;
    private String title;
    private String author;
    private Date createTime;
    private int views;
}
// 省略了getter、setter、toString方法,和構造方法

編寫mapper介面

public interface BlogMapper {
// 在mapper介面中寫對應的新增資料方法
    int addBlog (Blog blog);
}

編寫對應的mapper.xml檔案

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.molu.mapper.BlogMapper">
<insert id="addBlog" parameterType="Blog">
    insert into mybatis.blog (id,title,author,createTime,views)
    values (#{id},#{title},#{author},#{createTime},#{views});
</insert>
</mapper>

我們再寫一個工具類,使用UUID來生成隨機的 blogId

工具類

public class BlogGetId {
    public static String getId(){
        return UUID.randomUUID().toString().replaceAll("-","");
    }
}

在測試類中插入資料

public class MapperTest {
    @Test
    public void addBlogTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        // 插入資料
        mapper.addBlog(new Blog(BlogGetId.getId(),"Java入門","moluu",new Date(),9999));
        mapper.addBlog(new Blog(BlogGetId.getId(),"MySQl入門","lin",new Date(),1000));
        mapper.addBlog(new Blog(BlogGetId.getId(),"Mybatis入門","moluu",new Date(),9999));
        mapper.addBlog(new Blog(BlogGetId.getId(),"Spring入門","lin",new Date(),9999));
        // 進行增刪改操作,一定要記得提交事務。否則插入不了
        sqlSession.commit();
        sqlSession.close();
    }
}

插入成功

如果插入不成功的話檢查一下配置檔案是否有對其進行繫結,mapper.xml中的sql是否寫得有問題,是否漏掉了提交........

到這裡環境就準備好了

簡單使用

對於一些複雜的查詢,我們可能會指定多個查詢條件,但是這些條件可能存在也可能不存在,如果不使用持久層框架我們可能需要自己拼接SQL語句,十分的痛苦。不過MyBatis提供了動態SQL的功能來解決這個問題。

IF

我們先用一用最簡單的if

<mapper namespace="com.molu.mapper.BlogMapper">
<insert id="addBlog" parameterType="Blog">
    insert into mybatis.blog (id,title,author,createTime,views)
    values (#{id},#{title},#{author},#{createTime},#{views});
    <!-- if 一般定義在 crud標籤體中-->
    <if test="">
        
    </if>
</insert>
</mapper>

現在我們想要實現一個功能,如果我們不對查詢條件進行限制則返回所有的部落格,如果我們對條件進行限制 則返回對應條件的部落格。

使用 if實現該功能

  • 在mapper介面中寫對應的方法
public interface BlogMapper {
// 根據條件返回查詢結果
    List<Blog> queryBlogIF(Map map);
}
  • 修改mapper.xml檔案
<select id="queryBlogIF" parameterType="map" resultType="blog">
    select * from mybatis.blog where 1=1

    <if test="title != null">
        and title = #{title};
    </if>
    <if test="author != null">
        and author = #{author};
    </if>
</select>

我們使用了 if標籤,對查詢 sql進行了簡單的條件追加。

  • 編寫測試類
public class MapperTest {
    @Test
    public void queryBlogIFTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        HashMap map = new HashMap();
        List<Blog> blogs = mapper.queryBlogIF(map);
        for (Blog blog : blogs) {
            System.out.println(blog);
        }
        sqlSession.close();
    }
}

我們往查詢程式碼中傳入了一個空map,也就是不追加任何條件。

由於我們在 where子句寫了個 1=1 的條件,該條件是一定能夠被滿足的,執行後自然會返回所有的部落格。

  • 嘗試往 map中 put值(追加條件)
public class MapperTest {
    @Test
    public void queryBlogIFTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        HashMap map = new HashMap();
        // 現在 title有值了
        map.put("title","Java入門");
        List<Blog> blogs = mapper.queryBlogIF(map);
        for (Blog blog : blogs) {
            System.out.println(blog);
        }
        sqlSession.close();
    }
}

可以看到,在追加條件後 會返回對應條件的資料。

也就是通過 if標籤,在 test條件滿足時 往where子句中追加了條件。這就是最簡單的動態sql的使用

<if test="title != null">
    <!--test條件也很簡單,title有值就滿足了-->
	and title = #{title};
    <!--進而對 where子句進行sql的拼接,即追加條件-->
</if>

我們再玩一玩 if

現在我們查詢一下 author為 ‘lin’ 的部落格,也很簡單,往 map中 put一下 author的值即可。

//map.put("title","Java入門");
map.put("author","lin");

查詢結果:

很實用也很簡單的一個標籤不是麼?

Where

where標籤能夠使我們的sql拼接時,避免一些錯誤

在正常的開發中,我們是不會在where子句後面寫什麼 1=1 的,上面我只是為了方便演示偷了個懶才那麼寫

正常的話,應該是要將 1=1去掉的:

<select id="queryBlogIF" parameterType="map" resultType="blog">
    select * from mybatis.blog where
    <if test="title != null">
        and title = #{title};
    </if>
    <if test="author != null">
        and author = #{author};
    </if>
</select>

寫成這樣的話,if 標籤再拼接就有問題了,我們配置一下日誌實現,方便檢視拼接的sql

<settings>
    <!--方便起見,我們就不配log4j了-->
	<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

現在我們再執行一下測試類

在 where後面拼接了一個and,直接導致sql語句錯誤。

實際上還不怎麼好徹底解決這個問題,畢竟不是每次都使用一個條件。and自然是必不可少的。

MyBatis 有一個簡單且適合大多數場景的解決辦法。而在其他場景中,可以對其進行自定義以符合需求。而這,只需要一處簡單的改動。

where 元素只會在子元素返回任何內容的情況下才插入 “WHERE” 子句。而且,若子句的開頭為 “AND” 或 “OR”,where 元素也會將它們去除。

也就是說,我們只需要用一個where標籤將 if標籤包裹,就能夠避免在 where後拼接 and的問題,最重要的是不需要對我們的拼接sql進行任何改動。

<mapper namespace="com.molu.mapper.BlogMapper">
<select id="queryBlogIF" parameterType="map" resultType="blog">
    select * from mybatis.blog
    <where >
        <if test="title != null">
            and title = #{title};
        </if>
        <if test="author != null">
            and author = #{author};
        </if>
    </where>
</select>
</mapper>

再次執行測試類

可以看到,我們拼接sql開頭的and,在執行後直接就被去掉了,從根本上避免了上面的問題。

如果我們不追加條件where是會被自動去掉的,也就實現了 不追加條件則返回全部部落格的功能

如果 where 元素與你期望的不太一樣,你也可以通過自定義 trim 元素來定製 where 元素的功能。比如,和 where 元素等價的自定義 trim 元素為:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

如果where標籤沒辦法解決你的問題,你還可以使用 trim標籤來改變 where標籤的規則,進行一些簡單的自定義操作。這個就不再展開了,感興趣可以嘗試一下。

choose、when、otherwise

有時候,我們不想使用所有的條件,而只是想從多個條件中選擇一個使用。針對這種情況,MyBatis 提供了 choose 元素,它有點像 Java 中的 switch 語句。

還是上面的例子,但是策略變為:傳入了 “title” 就按 “title” 查詢,傳入了 “author” 就按 “author” 查詢的情形。若兩者都沒有傳入,就返回瀏覽量為 9999 的 Blog(這可能是管理員認為,與其返回大量的無意義隨機 Blog,還不如返回一些由管理員精選的 Blog)。

我們來實現一下,首先修改mapper.xml

<mapper namespace="com.molu.mapper.BlogMapper">
    <select id="queryBlogChoose" parameterType="map" resultType="blog">
        select * from mybatis.blog
        <where>
            <choose>
                <when test="title != null">
                    and title = #{title}
                </when>
                <when test="author != null">
                    and author = #{author}
                </when>
                <otherwise>
                    views = 9999
                </otherwise>
            </choose>
        </where>
    </select>
</mapper>

寫成這樣後,就不存在 不追加條件返回全部部落格了。它至少會滿足一個條件的,即便我們什麼條件都不追加,他也會將 otherwise標籤中的views = 9999拼接上,也就實現了與其返回大量的無意義隨機 Blog,還不如返回一些由管理員精選的 Blog

嘗試不追加條件

嘗試將條件全部追加

條件全部追加它只會執行最初滿足的條件,類似Java 中的 switch 語句。所以就不會存在什麼條件互斥的問題,它只會滿足一個條件,也就是第一個滿足的條件。

set

set 類似於where,它也是用來避免我們拼接sql時出錯的

set 元素會動態地在行首插入 SET 關鍵字,並會刪掉額外的逗號。因為我們進行sql拼接時,很難避免這個 “ , ” 的問題。

我們寫一個修改操作

  • 首先在mapper介面中寫對應的方法
public interface BlogMapper {
    // 根據id修改部落格
    int updateBlog(Map map);
}
  • 修改 mapper.xml檔案
<mapper namespace="com.molu.mapper.BlogMapper">
    <update id="updateBlog" parameterType="map">
        update mybatis.blog
        <set>
            <if test="title != null">
                title = #{title},
            </if>
            <if test="author != null">
                author = #{author},
            </if>
        </set>
        where id = #{id};
    </update>
</mapper>

看到這樣一個拼接sql,我們很明顯能夠感覺到會出錯,逗號明顯多了一個,最後它會拼接成:

update mybatis.bolg set title = #{title},author = #{author}, where id = #{id};

這樣的sql按道理是沒辦法執行的,但如果是在set標籤中,那就無所謂了。

  • 測試類
    @Test
    public void updateBlogTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        HashMap map = new HashMap();
        map.put("title","Web入門");
        map.put("author","lin");
        map.put("id","7280ac58a49040818f97cf3d59e598dd");
        mapper.updateBlog(map);
        sqlSession.commit();
        sqlSession.close();
    }
  • 測試 結果:

set 和 where一樣,你也可以使用 trim標籤來改變 set標籤的規則,進行一些簡單的自定義操作。

SQL片段

我們在演示上面這些標籤的時候,有一些sql語句頻繁被用到,我們可以使用 sql標籤將他抽取出來。實現 sql的複用。

比如這幾行sql,基本上一直在用。

<if test="title != null">
 	title = #{title}
</if>
<if test="author != null">
	author = #{author}
</if>

我們使用 sql標籤將他提取出來,取一個比較符合的id。

<sql id="TA_sql">
	<if test="title != null">
		title = #{title}
	</if>
	<if test="author != null">
		author != #{author}
	</if>
</sql>

之後我們需要用到這段 sql的時候,可以使用 include標籤將其引用過來。

    <select id="queryBlog" parameterType="map" resultType="blog">
        select * from mybatis.blog
        <where>
            <include refid="TA_sql"></include>
        </where>
    </select>

測試類

    @Test
    public void queryBlogTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        HashMap map = new HashMap();
        map.put("title","Web入門");
        List<Blog> blogs = mapper.queryBlog(map);
        for (Blog blog : blogs) {
            System.out.println(blog);
        }
        sqlSession.close();
    }

測試結果:

可以看到,通過這種引用sql片段的方式,也能夠達我們想要的效果 且更省事。

foreach

foreach 元素的功能非常強大,它允許你指定一個集合,宣告可以在元素體內使用的集合項(item)和索引(index)變數。它也允許你指定開頭與結尾的字串以及集合項迭代之間的分隔符。這個元素也不會錯誤地新增多餘的分隔符,看它多智慧!提示 你可以將任何可迭代物件(如 List、Set 等)、Map 物件或者陣列物件作為集合引數傳遞給 foreach。當使用可迭代物件或者陣列時,index 是當前迭代的序號,item 的值是本次迭代獲取到的元素。當使用 Map 物件(或者 Map.Entry 物件的集合)時,index 是鍵,item 是值。

看這些比較官方的文字還是看不出個所以然,我們仍然通過案例來理解。

既然要遍歷集合,那我們的UUID就不適用了,修改成1 2 3 4吧

  • 在 mapper介面中寫我們對應的方法
public interface BlogMapper {
    List<Blog> queryBlogForeach(Map map);
}
  • 編寫mapper.xml檔案
<mapper namespace="com.molu.mapper.BlogMapper">
    <select id="queryBlogForeach" resultType="blog" parameterType="map">
        select * from mybatis.blog
        <where>
            <!--使用了 foreach標籤-->
            <foreach collection="Fids" open="(" separator="or" close=")" item="Fid">
                id = #{Fid}
            </foreach>
        </where>
    </select>
</mapper>
  • 測試類
    @Test
    public void queryBlogTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        HashMap map = new HashMap();
        ArrayList Fids = new ArrayList();
        Fids.add(1);
        Fids.add(2);
        map.put("Fids",Fids);
        mapper.queryBlogForeach(map);
        sqlSession.close();
    }
  • 測試結果:

到這裡大致應該能猜 foreach標籤到底做了什麼

其實 foreach標籤做的事情很簡單,無非也是拼接sql,只是在拼接前 執行了一個遍歷集合的操作。

 <!--使用了 foreach標籤-->
            <foreach collection="Fids" item="Fid" open="(" separator="or" close=")" >
                id = #{Fid}
            </foreach>

如果我們要實現通過傳入的 id來查詢指定的部落格功能,sql語句應該寫成這樣:

select * from mybatis.blog where 1=1 and(id = #{id} or id = #{id} or .....)

這樣寫的話,怎麼看怎麼不舒服。一來是寫死了,有侷限性 ,二來這樣寫sql 不免讓人覺得有點low…

而我們通過 foreach標籤的話可以做到動態的傳入指定的 id。

foreach的屬性著手

  • collection,表示要遍歷的集合名字,也就是我們要在測試類中要建立的 list
  • item,表示遍歷出來的每一個元素的名字,也就是我們要寫在下面條件中的 引數(Fid)
  • open,表示拼接 sql的起始
  • separator,表示拼接 sql時使用的分隔符
  • close,表示拼接 sql的結尾

看完這些你應該對foreach標籤有了一定的瞭解。

也就是說,我們通過map傳入的list集合,會被 foreach標籤進行遍歷

遍歷出來的每一個元素 都會被作為條件傳入到我們的 where子句後面。

由於我們使用了 where標籤,所以哪怕我們的集合中什麼都沒有,sql也是能夠執行成功的。

如果集合中有元素,該元素就會被foreach 標籤拿去拼接。

我們又對open、separator close、進行了定義,那麼拼接進去的sql 也就不會出問題了。

每個傳入的元素之間會使用 or 進行分隔,起始和結尾是一對括號,也就實現了 ( id = #{Fid} or id = #{iFd} or …)的拼接。

所謂的的動態SQL本質還是SQL語句,只是我們可以在SQL層面,執行一些邏輯程式碼

動態SQL就是在拼接SQL語句,我們只需要保證SQL的正確性,按照SQL的格式想辦法對其進行排列組合就可以了


放鬆一下眼睛

原圖地址

畫師主頁


相關文章