MyBatis 動態 SQL

MyBatis 的動態 SQL 功能可以幫助我們根據不同條件拼接 SQL 語句,並自動處理 SQL 語法,動態 SQL 功能通過 OGNL(Object-Graph Navigation Language) 表達式和以下幾個標籤實現,下方詳細介紹。

首先列出本文涉及到的數據表 DDL、entity 對象和 Mybatis 基本配置。

  • DDL (源自 MySQL 官方演示用的 world_x ,簡單修改欄位名更符合我們熟知的開發規範)

    CREATE TABLE country
    (
        primary_code   VARCHAR(3)  DEFAULT '' NOT NULL PRIMARY KEY,
        country_name   VARCHAR(52) DEFAULT '' NOT NULL,
        capital        INT                    NULL,
        secondary_code VARCHAR(2)  DEFAULT '' NOT NULL
    );
    
  • Country 對象

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Country implements Serializable {
    
        /** 主要國家程式碼 */
        private String primaryCode;
    
        /** 國家名 */
        private String countryName;
    
        /** 首都ID */
        private Integer capital;
    
        /** 次要國家程式碼 */
        private String secondaryCode;
    
        private static final long serialVersionUID = 1L;
    
    }
    
  • Mapper 介面大致內容:

    package pers.cncsl.ft.mybatis.mapper;
    
    import org.apache.ibatis.annotations.Param;
    import org.springframework.stereotype.Repository;
    import pers.cncsl.ft.mybatis.entity.Country;
    
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    /**
     * 國家Mapper
     */
    @Repository
    public interface CountryMapper {
    	//...
    }
    
  • mapper.xml 文件大致內容:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "//mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="pers.cncsl.ft.mybatis.mapper.CountryMapper">
    
        <resultMap id="BaseResultMap" type="pers.cncsl.ft.mybatis.entity.Country">
            <id column="primary_code" jdbcType="VARCHAR" property="primaryCode"/>
            <result column="country_name" jdbcType="VARCHAR" property="countryName"/>
            <result column="capital" jdbcType="INTEGER" property="capital"/>
            <result column="secondary_code" jdbcType="VARCHAR" property="secondaryCode"/>
        </resultMap>
    
        <!--其他 MyBatis 標籤-->
    
    </mapper>
    

if

if 是最基本的動態 SQL 標籤,其 test 屬性是一個用於判斷輸入參數的 OGNL 表達式,當條件判斷為真時會拼接其中的內容。與程式語言不同,mybatis 沒有 else 標籤,if 標籤僅用於最基本的條件判斷,如果有多個並列的條件需要連續使用相應數量的 if 標籤。例如根據主要國家程式碼或(和)國家名搜索:

/**
* 根據主鍵或國家名查詢一條記錄
*
* @param primaryKey 主鍵
* @param countryName 國家名
* @return 查詢結果
*/
Country selectByCodeOrName(@Param("primaryKey") String primaryKey, @Param("countryName") String countryName);
<select id="selectByCodeOrName" resultMap="BaseResultMap">
    SELECT primary_code, country_name, capital, secondary_code
    FROM country
    WHERE 1 = 1
    <if test="primaryCode != null">
        and primary_code = #{primaryCode, jdbcType=VARCHAR}
    </if>
    <if test="countryName != null">
        and country_name = #{countryName, jdbcType=VARCHAR}
    </if>
</select>

如下三次調用:

Country resultByCode = mapper.selectByCodeOrName("CHN", null);
Country resultByName = mapper.selectByCodeOrName(null, "China");
Country resultByCodeAndName = mapper.selectByCodeOrName("CHN", "China");

從 Mybatis 的日誌中可以看到分別執行的 SQL 語句是:

SELECT primary_code, country_name, capital, secondary_code FROM country WHERE 1 = 1 and primary_code = ?
SELECT primary_code, country_name, capital, secondary_code FROM country WHERE 1 = 1 and country_name = ?
SELECT primary_code, country_name, capital, secondary_code FROM country WHERE 1 = 1 and primary_code = ? and country_name = ?

choose

choose 標籤的應用場景是多個條件選擇一個,類似程式語言的 switch 語法。它有 when 和 otherwise 兩個子標籤:

  • when 子標籤和 if 類似,test 屬性用於判斷 OGNL 表達式,一個 otherwise 標籤內可以有任意數量、只會命中第一個 test 屬性為真的。
  • otherwise 子標籤類似 switch 的 default,如果前方所有的 when 子標籤條件判斷都為假,才會拼接上 otherwise 子標籤里的內容。

例如上面的例子可以進一步改造為(介面內容不變):

<select id="selectByCodeOrName" resultMap="BaseResultMap">
    SELECT primary_code, country_name, capital, secondary_code
    FROM country
    WHERE 1 = 1
    <choose>
        <when test="primaryCode != null">
            and primary_code = #{primaryCode, jdbcType=VARCHAR}
        </when>
        <otherwise>
            and country_name = #{countryName, jdbcType=VARCHAR}
        </otherwise>
    </choose>
</select>

和之前同樣的參數再調用三次,執行的 SQL 語句分別是:

SELECT primary_code, country_name, capital, secondary_code FROM country WHERE 1 = 1 and primary_code = ?
SELECT primary_code, country_name, capital, secondary_code FROM country WHERE 1 = 1 and country_name = ?
SELECT primary_code, country_name, capital, secondary_code FROM country WHERE 1 = 1 and primary_code = ?

trim

trim 標籤用於避免使用 if 或 choose 時為了 SQL 語法的正確性而不得不插入類似 1=1 and 這種語句,保證生成的 SQL 更乾淨、優雅,where 和 set 標籤是 trim 的一種具體用法。

where

如果 where 標籤內的其他標籤有返回值,就在標籤位置插入一個 where,並剔除後續字元串開頭的 AND 和 OR。

在之前的兩個例子中,為了保證生成正確的 SQL,我們加入了 WHERE 1=1 這樣的程式碼段,可以通過 where 標籤來優化(介面內容不變):

<select id="selectByCodeOrName" resultMap="BaseResultMap">
    SELECT primary_code, country_name, capital, secondary_code
    FROM country
    <where>
        <choose>
            <when test="primaryCode != null">
                and primary_code = #{primaryCode, jdbcType=VARCHAR}
            </when>
            <otherwise>
                and country_name = #{countryName, jdbcType=VARCHAR}
            </otherwise>
        </choose>
    </where>
</select>

改造後仍然用之前的參數調用,執行的 SQL 語句分別是:

SELECT primary_code, country_name, capital, secondary_code FROM country WHERE primary_code = ?
SELECT primary_code, country_name, capital, secondary_code FROM country WHERE country_name = ?
SELECT primary_code, country_name, capital, secondary_code FROM country WHERE primary_code = ?

可以看出,MyBatis 已經幫我們在生成的 SQL 中插入了 WHERE。

set

如果 set 標籤內的其他標籤有返回值,就在標籤位置插入一個 set,並剔除後續字元串結尾處的逗號。

很常見的需求是僅更新數據表中的指定欄位,這些欄位需要通過 if 標籤處理,而 set 標籤用於插入一個 SET 關鍵字:

/**
* 根據主鍵和入參的其他數據更新一條記錄,入參為 null 的欄位不變更
*
* @param entity 入參數據
* @return 更新結果
*/
int updateByPrimaryKeySelective(Country entity);
<update id="updateByPrimaryKeySelective" parameterType="pers.cncsl.ft.mybatis.entity.Country">
    UPDATE country
    <set>
        <if test="countryName != null">
            country_name = #{countryName, jdbcType=VARCHAR},
        </if>
        <if test="capital != null">
            capital = #{capital, jdbcType=INTEGER},
        </if>
        <if test="secondaryCode != null">
            secondary_code = #{secondaryCode, jdbcType=VARCHAR},
        </if>
    </set>
    WHERE primary_code = #{primaryCode, jdbcType=VARCHAR}
</update>

如下調用:

Country update = Country.builder().primaryCode(PRIMARY_CODE).countryName("中國").build();
int rows = mapper.updateByPrimaryKeySelective(update);

執行的 SQL 為:

UPDATE country SET country_name = ? WHERE primary_code = ?

Mybatis 幫我們插入了 SET 關鍵字、並刪除了後續多餘的逗號。

trim 屬性詳解

trim 標籤有以下四個屬性:

  • prefix:當 trim 內有內容時,會給內容增加 prefix 指定的前綴
  • prefixOverrides:當 trim 內有內容時,會將內容中匹配的前綴字元串取掉
  • suffix:當 trim 內有內容時,會給內容增加 suffix 指定的後綴
  • suffixOverrides:當 trim 內有內容時,會將內容中匹配的後綴字元串取掉

所以 where 標籤和 set 標籤實際相當於:

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

<trim prefix="SET" suffixOverrides="," > ... </trim>

foreach

顧名思義,foreach 標籤用於實現集合遍歷,它有以下屬性:

  • collection:要迭代遍歷的屬性名。
  • item:變數名,每次迭代時從集合中取出的值的名稱。
  • index:索引值,對 List 集合值為當前索引值,對 Map 集合值為當前 key。
  • open:整個循環內容開頭處添加的字元串。
  • close:整個循環內容結尾處添加的字元串。
  • separator:每次循環的分隔符。

例如根據主鍵批量查詢:

/**
* 根據入參的主鍵List集合批量查詢,使用 List 較為簡單。
*
* @param keys 主鍵List集合
* @return 查詢結果
*/
List<Country> selectByPrimaryCodeList(List<String> keys);
<select id="selectByPrimaryCodeList" parameterType="java.util.Collection"
        resultType="pers.cncsl.ft.mybatis.entity.Country">
    SELECT primary_code, country_name, capital, secondary_code
    FROM country
    <where>
        <if test="list != null and !list.isEmpty()">
            primary_code IN
            <foreach collection="list" open="(" close=")" separator="," item="code">
                #{code}
            </foreach>
        </if>
    </where>
</select>

注意上面的 where、if 和 foreach 三種標籤的組合使用,能避免入參錯誤導致 MyBatis 拼接出錯誤的 SQL 語句,不過入參為 null 或空集合時會查出全表。編碼時對於這種需求應該小心對待,盡量不傳入 null 或空集合。

collection 屬性的值是 Mybatis 中的參數名,通常用 @Param 註解指定,不指定時 Mybatis 會對不同類型的參數添加一個默認的名稱,數組和集合有關的默認參數名規則如下:

  • 數組類型 array
  • Collection 集合(java.util.Collection 的實現類)都為 collection
  • List 也可用 list

仔細下方內容,重點在於 Mapper 介面中函數入參為數組、List 集合和 Set 集合時 xml 文件中 foreach 標籤的屬性和內容:

/**
* 根據入參的主鍵數組批量查詢,使用數組是最基礎的情況,使用場景不多。
*
* @param keys 主鍵數組
* @return 查詢結果
*/
List<Country> selectByPrimaryArray(String[] keys);

/**
* 根據入參的主鍵List集合批量查詢,使用 List 較為簡單。
*
* @param keys 主鍵List集合
* @return 查詢結果
*/
List<Country> selectByPrimaryCodeList(List<String> keys);

/**
* 根據入參的主鍵Set集合批量查詢,使用 Set 可以保證集合元素唯一。
*
* @param keys 主鍵Set集合
* @return 查詢結果
*/
List<Country> selectByPrimaryCodeSet(Set<String> keys);
<select id="selectByPrimaryArray" parameterType="java.util.Collection"
        resultType="pers.cncsl.ft.mybatis.entity.Country">
    SELECT primary_code, country_name, capital, secondary_code
    FROM country
    <where>
        <if test="array != null and array.length gt 0">
            primary_code IN
            <foreach collection="array" open="(" close=")" separator="," item="code">
                #{code}
            </foreach>
        </if>
    </where>
</select>

<select id="selectByPrimaryCodeList" parameterType="java.util.Collection"
        resultType="pers.cncsl.ft.mybatis.entity.Country">
    SELECT primary_code, country_name, capital, secondary_code
    FROM country
    <where>
        <if test="list != null and !list.isEmpty()">
            primary_code IN
            <foreach collection="list" open="(" close=")" separator="," item="code">
                #{code}
            </foreach>
        </if>
    </where>
</select>

<select id="selectByPrimaryCodeSet" parameterType="java.util.Collection"
        resultType="pers.cncsl.ft.mybatis.entity.Country">
    SELECT primary_code, country_name, capital, secondary_code
    FROM country
    <where>
        <if test="collection != null and !collection.isEmpty()">
            primary_code IN
            <foreach collection="collection" open="(" close=")" separator="," item="code">
                #{code}
            </foreach>
        </if>
    </where>
</select>

foreach 還可實現批量插入和動態更新功能,不過這兩個功能在開發中用到的場景不多,就不詳細說了。

Tags: