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" "http://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 還可實現批量插入和動態更新功能,不過這兩個功能在開發中用到的場景不多,就不詳細說了。
