MyBatis 動態查詢語句詳解:讓 SQL 更靈活可控
在日常的數據庫操作中,我們經常會遇到需要根據不同條件拼接 SQL 語句的場景。比如查詢用戶時,可能需要根據姓名、年齡、性別等多個條件進行篩選,而這些條件往往是動態變化的 —— 有時需要按姓名查,有時需要按年齡查,有時又需要組合多個條件。如果手動拼接 SQL,不僅容易出錯,還會讓代碼變得臃腫難維護。MyBatis 的動態查詢語句正是為解決這一問題而生,它能根據參數的不同自動拼接 SQL 片段,讓 SQL 編寫更加靈活高效。
一、動態查詢的核心價值
動態查詢是 MyBatis 的核心特性之一,它允許我們在 XML 映射文件或注解中,通過一系列標簽控制 SQL 片段的拼接邏輯。其核心價值體現在三個方面:
-
減少冗余代碼:無需為不同條件組合編寫大量重復的 SQL 語句。
-
避免 SQL 注入風險:MyBatis 的動態標簽會自動處理參數拼接,比手動字符串拼接更安全。
-
提升代碼可讀性:將條件判斷邏輯與 SQL 語句分離,讓業務邏輯更清晰。
舉個簡單的例子:查詢用戶列表時,可能需要根據姓名(name)和年齡(age)篩選。如果沒有動態查詢,我們可能需要編寫 3 條 SQL(只查 name、只查 age、同時查 name 和 age);而有了動態查詢,1 條 SQL 就能搞定所有場景。
二、常用動態查詢標簽詳解
MyBatis 提供了一套完整的動態 SQL 標簽,涵蓋了大多數條件判斷場景。下面介紹最常用的幾個標簽及其用法。
1. <if>
:條件判斷的基礎
<if>
標簽是動態查詢中最常用的標簽,用于根據參數值決定是否拼接某個 SQL 片段。其語法如下:
<if test="條件表達式">SQL片段
</if>
示例:根據姓名和年齡查詢用戶
<select id="selectUser" resultType="User">SELECT * FROM userWHERE 1=1<if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if><if test="age != null">AND age = #{age}</if>
</select>
這里的WHERE 1=1
是為了避免當所有<if>
條件都不滿足時,SQL 出現WHERE
后無內容的語法錯誤。當name
不為空時,會拼接AND name LIKE ...
;當age
不為空時,會拼接AND age = ...
。
2. <choose>
+<when>
+<otherwise>
:多條件分支選擇
當需要實現 “多條件選其一” 的邏輯時,可以使用<choose>
標簽(類似 Java 中的switch
),配合<when>
(類似case
)和<otherwise>
(類似default
)標簽使用。
示例:優先按姓名查詢,若姓名為空則按年齡查詢,否則查詢所有
<select id="selectUserByCondition" resultType="User">SELECT * FROM user<where><choose><when test="name != null and name != ''">name LIKE CONCAT('%', #{name}, '%')</when><when test="age != null">age = #{age}</when><otherwise>1=1 <!-- 當所有條件都不滿足時,查詢所有數據 --></otherwise></choose></where>
</select>
<where>
標簽會自動處理 SQL 片段開頭的AND
或OR
,避免語法錯誤。比如當第一個<when>
條件滿足時,拼接的name LIKE ...
前不會有多余的AND
。
3. <trim>
:自定義 SQL 片段拼接規則
<trim>
標簽可以通過屬性自定義 SQL 片段的前綴、后綴,以及需要去掉的前綴或后綴字符,比<where>
更靈活。常用屬性:
-
prefix
:給拼接的 SQL 片段添加前綴 -
suffix
:給拼接的 SQL 片段添加后綴 -
prefixOverrides
:去掉片段開頭的指定字符 -
suffixOverrides
:去掉片段結尾的指定字符
示例:用<trim>
實現<where>
的功能
<select id="selectUser" resultType="User">SELECT * FROM user<trim prefix="WHERE" prefixOverrides="AND | OR"><if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if><if test="age != null">AND age = #{age}</if></trim>
</select>
當<trim>
內部有內容時,會添加WHERE
前綴,并去掉開頭的AND
或OR
,效果與<where>
一致。
4. <foreach>
:遍歷集合參數
當需要處理批量操作(如IN
查詢、批量插入)時,<foreach>
標簽非常有用。它可以遍歷數組或集合,將元素拼接成 SQL 片段。常用屬性:
-
collection
:指定要遍歷的集合參數名(如list
、array
或@Param
定義的名稱) -
item
:遍歷過程中每個元素的別名 -
index
:遍歷索引(可選) -
open
:片段開頭的字符 -
close
:片段結尾的字符 -
separator
:元素之間的分隔符
示例 1:批量查詢(IN 語句)
<select id="selectUserByIds" resultType="User">SELECT * FROM userWHERE id IN<foreach collection="ids" item="id" open="(" close=")" separator=",">#{id}</foreach>
</select>
當ids
為[1,2,3]
時,會拼接成WHERE id IN (1,2,3)
。
示例 2:批量插入
<insert id="batchInsertUser">INSERT INTO user (name, age) VALUES<foreach collection="list" item="user" separator=",">(#{user.name}, #{user.age})</foreach>
</insert>
當list
包含 3 個 User 對象時,會拼接成INSERT INTO user (name, age) VALUES (?,?), (?,?), (?,?)
。
5. <set>
:更新語句的動態處理
在更新操作中,<set>
標簽用于動態拼接SET
子句,會自動去掉多余的逗號。
示例:動態更新用戶信息
<update id="updateUser">UPDATE user<set><if test="name != null and name != ''">name = #{name},</if><if test="age != null">age = #{age},</if></set>WHERE id = #{id}
</update>
當name
和age
都不為空時,會拼接成UPDATE user SET name = ?, age = ? WHERE id = ?
,自動去掉age = ?
后的逗號。
三、實戰案例:綜合運用動態標簽
下面通過一個復雜案例,展示如何綜合運用上述標簽實現多條件組合查詢。
需求:查詢商品列表,支持按分類(category)、價格范圍(minPrice、maxPrice)、是否庫存(hasStock)篩選,且支持排序(sortField、sortType)。
Mapper 接口:
List<Product> selectProducts(@Param("category") String category,@Param("minPrice") BigDecimal minPrice,@Param("maxPrice") BigDecimal maxPrice,@Param("hasStock") Boolean hasStock,@Param("sortField") String sortField,@Param("sortType") String sortType
);
XML 映射文件:
<select id="selectProducts" resultType="Product">SELECT * FROM product<trim prefix="WHERE" prefixOverrides="AND | OR"><if test="category != null and category != ''">AND category = #{category}</if><if test="minPrice != null">AND price >= #{minPrice}</if><if test="maxPrice != null">AND price <= #{maxPrice}</if><if test="hasStock != null">AND stock > 0</if></trim><if test="sortField != null and sortField != '' and sortType != null">ORDER BY ${sortField} ${sortType} <!-- 排序字段用${},注意SQL注入風險 --></if>
</select>
說明:
-
用
<trim>
處理查詢條件,避免多余的AND
-
用
<if>
判斷每個篩選條件是否生效 -
排序部分通過
<if>
控制是否拼接ORDER BY
,注意排序字段使用${}
(因為#{}
會加引號,導致語法錯誤),但需確保sortField
和sortType
是可信參數,避免 SQL 注入。
四、注意事項與最佳實踐
- 參數判斷的細節:
-
字符串判斷:
test="name != null and name != ''"
(先判斷非空,再判斷非空字符串) -
數字判斷:
test="age != null"
(無需判斷空字符串) -
布爾值判斷:
test="hasStock != null and hasStock"
(直接用參數名判斷 true/false)
- SQL 注入風險:
-
動態 SQL 中,
#{}
會自動加引號,適合參數值;${}
直接拼接 SQL,適合表名、字段名等,但需嚴格校驗參數,避免注入。 -
排序、分組等場景需用
${}
時,建議限制可選值(如sortField
只能是id
、price
等已知字段)。
- 代碼可讀性:
-
復雜動態 SQL 建議拆分到 XML 文件,而非注解中
-
給參數添加
@Param
注解,避免使用arg0
、arg1
-
合理使用換行和縮進,保持 SQL 結構清晰
- 性能優化:
-
避免過度復雜的動態 SQL,否則可能影響數據庫優化器生成執行計劃
-
頻繁執行的動態 SQL,建議通過 MyBatis 的二級緩存緩存結果
五、總結
MyBatis 的動態查詢語句通過<if>
、<choose>
、<foreach>
等標簽,完美解決了 SQL 條件動態拼接的問題,讓開發者能更專注于業務邏輯而非 SQL 語法細節。掌握這些標簽的用法,不僅能減少代碼量,還能提升系統的靈活性和可維護性。
在實際開發中,建議結合具體業務場景選擇合適的標簽,同時注意參數校驗和 SQL 注入風險,讓動態 SQL 成為提升開發效率的利器而非隱患。