MyBatis的那些動態Sql

1.前言

Mybatis的動態Sql區別於靜態Sql的一點就是在程序運行期間根據特定的條件生成的Sql。這裏的條件和一般程序語言一樣,提供了順序、判斷、循環的執行流。爲此Mybatis也提供了if、choose (when, otherwise)、trim (where, set)和foreach標籤以及我前文Mybatis何時了,佔位符你知多少提到的Mybatis佔位符 。通過這些標籤,我們再也不用根據不同條件拼接 SQL 語句,也能避免丟失必要的空格以及列表最後一個列名的逗號。只有真正使用過JDBC的人才能深刻體會到拼接Sql是一件多麼痛苦的事。

然而,使用動態 SQL 並非一件易事,但藉助可用於任何 SQL 映射語句中的強大的動態 SQL 語言,MyBatis 顯著地提升了這一特性的易用性。那麼我們今天就聊一下Mybatis動態Sql的那點事。

2.環境準備

2.1 sql準備

爲了方便大家實驗,我們仍然準備了sql方便大家實驗。

--創建用戶表
CREATE TABLE USER (
  ID INT(11) NOT NULL AUTO_INCREMENT,
  USERNAME VARCHAR(32) NOT NULL COMMENT '用戶名稱',
  BIRTHDAY DATETIME DEFAULT NULL COMMENT '生日',
  SEX CHAR(1) DEFAULT NULL COMMENT '性別',
  ADDRESS VARCHAR(256) DEFAULT NULL COMMENT '地址',
  PRIMARY KEY  (ID)
) ENGINE=INNODB DEFAULT CHARSET=UTF8;
 
--創建用戶數據
INSERT  INTO USER(ID,USERNAME,BIRTHDAY,SEX,ADDRESS) VALUES 
(41,'老王','2018-02-27 17:47:08','男','北京'),
(42,'小二王','2018-03-02 15:09:37','女','北京三元橋'),
(43,'小三王','2018-03-04 11:34:34','女','北京三元橋'),
(45,'李四','2018-03-04 12:04:06','男','北京三元橋'),
(46,'老王','2018-03-07 17:37:26','男','北京四元橋'),
(48,'小馬寶莉','2018-03-08 11:44:00','女','北京五元橋');
COMMIT;

2.2 Pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>studycode</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
</project>


2.3 Java對象

public class User implements Serializable {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    //get/set
    //toString()
}

2.4 sqlMapConfig配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--mybatis的主配置文件-->
<configuration>
 
    <properties resource="jdbcConfig.properties"/>
    <settings>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>
    
    <typeAliases>
        <typeAlias type="cn.surpass.domain.User" alias="user"/>
    </typeAliases>
 
    <!--配置環境-->
    <environments default="mysql">
        <!--配置環境-->
        <environment id="mysql">
            <!--配置事務類型-->
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <!--配置連接數據庫的基本信息-->
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
 
    <!--指定映射配置文件的位置,映射配置文件指的是每個獨立的配置文件-->
    <mappers>
        <package name="cn.surpass.skill.dao"/>
    </mappers>
</configuration>

 

2.5 Mapper文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.surpass.skill.dao.UserDao">
 
</mapper>

2.6 測試類

public class PlaceholderTest {
    private InputStream in;
    private SqlSession sqlSession;
    private SqlSessionFactory factory;
    @Before
    public void init() throws IOException {
        in= Resources.getResourceAsStream("SqlMapConfig.xml");
        factory = new SqlSessionFactoryBuilder().build(in);
        sqlSession = factory.openSession();
    }
 
    @After
    public void destroy()throws Exception{
        sqlSession.commit();
        sqlSession.close();
        in.close();
    }
    
    @Test
    public void test1(){
        
    }
}

到目前爲止,環境就算準備好了,接下來我們就開始體驗一把Mybatis爲我們提供的那些便利吧。

3. 初識動態Sql

3.1 if標籤

3.1.1 基本用法

使用動態 SQL 最常見情景是根據條件包含 where 子句的一部分。例如下面的例子,

3.1.2 其他情況

如果傳入值爲null,則沒有拼接用戶名的Sql語句,如下圖。

3.1.3 注意事項

if標籤使用起來是不是很方便,但是不是所有的情況都適合使用if標籤。看下面的例子,有可能真有可能就刪庫跑路了。

<delete id="deleteUserByName" parameterType="string">
    delete from USER where 1 = 1
    <if test="name != null">
        and like #{value}
    </if>
</delete>

3.2 choose、when、otherwise

上面提到if語句,熟悉編程的同學應該知道還有if...else...語句,那麼Mybatis也提供了類似的語法,那便是choose/when/otherwise語法。例如下面的例子,如果有姓名,則按照姓名查詢;如果姓名傳入值爲空,則按照年齡查詢。如果還是用if標籤貌似不能滿足我們的要求。

此時,如果我們使用choose/when/otherwise語句就能實現如下功能。 當然如果name傳入爲空,sex傳入男,應該就會將sex的條件輸出。感興趣的可以嘗試一下。

3.3 trim (where, set)

3.3.1 使用背景

通過前面的例子我們發現,當使用if標籤或者choose標籤的時候都要在where標籤後面加上一個1=1,目的是爲了使sql語句正確。大家可以想一下,如果不加這個條件會是什麼效果?

<select id="queryByUserName" resultType="user">
    SELECT * FROM USER WHERE
    <if test="value != null">
        USERNAME LIKE #{value}
    </if>
</select>

通過分析我們知道,當value值(name)不爲空的時候,Sql語句爲【SELECT * FROM USER WHERE USERNAME LIKE #{value}】,貌似沒有什麼問題,但是當value值(name)爲空的時候,Sql語句就變成【SELECT * FROM USER WHERE】,那麼這條語句顯然有問題,所以就有了前面的1=1。然而作爲一個牛逼的框架絕對不會允許出現這些無用的代碼。所以就提供了trim(where/set)標籤。

3.3.2 where標籤

我們改造一下3.1.1的例子,得到下面的情景。我們發現,where 元素只會在子元素返回任何內容的情況下才插入 “WHERE” 子句。而且,若子句的開頭爲 “AND” 或 “OR”,where 元素也會將它們去除。

3.3.3 set標籤

用於動態更新語句的類似解決方案叫做 setset 元素可以用於動態包含需要更新的列,忽略其它不更新的列。這個主要是用於update語句中。通過下圖我們發現set標籤會將if標籤的結尾的逗號刪除。這裏,大家可以自行嘗試一下如果逗號寫在語句的前面是什麼樣子的情況。

3.3.4 trim標籤

通過上面的實例我們看到了where標籤和set標籤的強大,他們可以爲我們節省了不少的考慮。正如mybatis中文文檔中描述的那樣:你應該能理解根據不同條件拼接 SQL 語句有多痛苦,例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最後一個列名的逗號。在寫這篇文章之前我一直以爲trim其實不是一個標籤,他只是where和set的概括。當我查了文檔之後發現他比where和set更加強大,換句話說where標籤和set標籤只是trim標籤的兩個特例。

在如下的業務中,我們就得使用trim標籤了。例如:現在對於查詢的結果進行排序,如果傳入兩個列名col1和col2。如果傳入的列名不爲空,則加入到排序規則。按照我們之前學到的可以有如下的Sql。

<select id="queryAll" resultType="user">
    SELECT * FROM USER order by 
    <if test="col1 != null">
        ${col1},
    </if>
    <if test="col2 != null">
        ${col2}
    </if>
</select>

我們可以分析一下,如果col1和col2都傳入值得情況下,有Sql語句【SELECT * FROM USER order by ${col1},${col2}】,這樣貌似沒有什麼問題,然而當我們讓${col2}爲空或者都爲空,那Sql就有問題了。如下圖:

經過對trim的瞭解,我們對於上述代碼換一種思路。如下圖,完美解決了這個問題。

對於trim標籤,這裏提供了四個參數,解釋如下:

prefix:前綴,如果trim標籤爲空則刪除前綴
prefixOverrides:trim中拼接的Sql如果以此指定的前綴開始,則刪除此前綴,多個用|分割。
suffix:後綴,如果trim標籤爲空則刪除後綴,
suffixOverrides:trim中拼接的Sql如果以此指定的前綴結尾,則刪除此前綴,多個用|分割。

爲了加固對於trim四個參數的瞭解,我們通過trim重寫一下where標籤的功能:

<trim prefix="where" prefixOverrides="AND | OR " suffixOverrides="AND | OR ">
            
</trim>

3.4 foreach標籤

動態 SQL 的另一個常見使用場景是對集合進行遍歷(尤其是在構建 IN 條件語句的時候)。

foreach 元素的功能非常強大,它允許你指定一個集合,聲明可以在元素體內使用的集合項(item)和索引(index)變量。它也允許你指定開頭與結尾的字符串以及集合項迭代之間的分隔符。這個元素也不會錯誤地添加多餘的分隔符,看它多智能!

提示 你可以將任何可迭代對象(如 List、Set 等)、Map 對象或者數組對象作爲集合參數傳遞給 foreach。當使用可迭代對象或者數組時,index 是當前迭代的序號,item 的值是本次迭代獲取到的元素。當使用 Map 對象(或者 Map.Entry 對象的集合)時,index 是鍵,item 是值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章