MyBatis 动态 SQL 完全指南
一、什么是动态 SQL?
动态 SQL 是 MyBatis 最强大的特性之一 。它允许我们在 XML 映射文件中,根据传入参数的不同,动态地拼接 SQL 语句 ,从而避免在 Java 代码中手动拼接字符串(如 WHERE 1=1 AND name = ?)带来的繁琐、易错和 SQL 注入风险。
简单来说:动态 SQL 让我们在 XML 中写"逻辑代码"(如 if、choose、foreach),让 SQL 能够"智能"地适应不同的查询条件。
二、为什么需要动态 SQL?
在实际业务中,查询条件往往是动态的:
- 用户可能只填了"品牌",没填"价格区间"。
- 用户可能选了排序字段,也可能没选。
- 用户可能传了多个 ID 进行批量查询。
如果不用动态 SQL,我们只能写多个不同的 SQL 方法,或者在 Java 中用 StringBuilder 拼接 SQL,不仅代码臃肿,还容易出错。
MyBatis 的动态 SQL 机制,将 SQL 的构建逻辑移到了 XML 中,利用 OGNL(Object-Graph Navigation Language)表达式 来评估条件,实现了声明式的 SQL 构造。
三、核心标签详解
| 标签 | 作用 | 类比 Java |
|---|---|---|
<if> |
条件判断,标签体成立则拼接 SQL | if |
<choose>/<when>/<otherwise> |
多分支选择,只选一个 | if-else if-else / switch |
<where> |
智能处理 WHERE 关键字和多余的 AND/OR |
无(MyBatis 特有) |
<set> |
智能处理 SET 关键字和多余的逗号 |
无(MyBatis 特有) |
<trim> |
灵活的字符串裁剪/拼接(<where>和<set>的底层) |
无(MyBatis 特有) |
<foreach> |
遍历集合,常用于 IN 查询或批量插入 |
for 循环 |
<bind> |
从 OGNL 表达式创建变量,供 SQL 使用 | 局部变量 |
四、条件判断标签
1. <if> 标签
最基本的条件标签,test 属性返回 true 时,拼接标签体内的 SQL。
基本语法
xml
<if test="条件表达式">
SQL 片段
</if>
示例:多条件查询
xml
<select id="selectByCondition" resultType="car">
SELECT * FROM t_car
<where>
<if test="brand != null and brand != ''">
AND brand LIKE CONCAT('%', #{brand}, '%')
</if>
<if test="carType != null and carType != ''">
AND car_type = #{carType}
</if>
<if test="minPrice != null">
AND guide_price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND guide_price <= #{maxPrice}
</if>
</where>
</select>
易错点:空字符串校验
xml
<!-- ❌ 错误:只判断了 null,没判断空字符串 -->
<if test="brand != null">
AND brand = #{brand}
</if>
<!-- ✅ 正确:同时判断 null 和空字符串 -->
<if test="brand != null and brand != ''">
AND brand = #{brand}
</if>
2. <choose>/<when>/<otherwise> 标签
类似 Java 的 switch 语句,只选择第一个满足条件的分支。
xml
<select id="selectByKeyword" resultType="car">
SELECT * FROM t_car
<where>
<choose>
<when test="type == 'brand'">
AND brand LIKE CONCAT('%', #{keyword}, '%')
</when>
<when test="type == 'carNum'">
AND car_num = #{keyword}
</when>
<otherwise>
AND brand LIKE CONCAT('%', #{keyword}, '%')
</otherwise>
</choose>
</where>
</select>
对比 <if>:
<if>:多个条件可以同时成立,同时拼接。<choose>:多个条件中最多只有一个生效。
五、智能构建标签
3. <where> 标签
自动处理 WHERE 关键字和多余的 AND/OR,避免 SQL 语法错误。
场景:多个条件动态拼接
xml
<select id="selectByCondition" resultType="car">
SELECT * FROM t_car
<where>
<if test="brand != null and brand != ''">
AND brand = #{brand}
</if>
<if test="carType != null and carType != ''">
AND car_type = #{carType}
</if>
</where>
</select>
智能行为:
- 如果没有条件成立,不会生成
WHERE。 - 如果第一个条件是
AND brand = ...,<where>会自动去掉开头的AND,生成WHERE brand = ...。
等价的手动写法(不推荐)
xml
SELECT * FROM t_car
WHERE 1=1
<if test="brand != null">
AND brand = #{brand}
</if>
4. <set> 标签
专门用于 UPDATE 语句,自动处理 SET 关键字和多余的逗号。
xml
<update id="updateSelective">
UPDATE t_car
<set>
<if test="brand != null and brand != ''">
brand = #{brand},
</if>
<if test="carType != null and carType != ''">
car_type = #{carType},
</if>
<if test="guidePrice != null">
guide_price = #{guidePrice},
</if>
</set>
WHERE id = #{id}
</update>
智能行为 :如果只有一个条件成立,<set> 会自动去掉结尾多余的逗号。
5. <trim> 标签 ------ 底层万能标签
<where> 和 <set> 的本质都是 <trim> 的简写。
语法:
xml
<trim prefix="前缀" suffix="后缀" prefixOverrides="去掉前缀" suffixOverrides="去掉后缀">
SQL 内容
</trim>
用 <trim> 实现 <where>
xml
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="brand != null">
AND brand = #{brand}
</if>
</trim>
如果条件成立,生成的 SQL 为 WHERE brand = #{brand}(AND 被自动去掉)。
用 <trim> 实现 <set>
xml
<trim prefix="SET" suffixOverrides=",">
<if test="brand != null">
brand = #{brand},
</if>
</trim>
如果条件成立,生成的 SQL 为 SET brand = #{brand}(末尾逗号被自动去掉)。
六、迭代标签
6. <foreach> 标签
遍历集合或数组,常用于:
IN查询 :WHERE id IN (1, 2, 3)- 批量插入 :
INSERT INTO ... VALUES (..), (..)
语法
xml
<foreach collection="集合名" item="元素名" index="索引名"
open="开头字符" close="结尾字符" separator="分隔符">
#{元素名}
</foreach>
| 属性 | 说明 | 是否必须 |
|---|---|---|
collection |
集合/数组的名称 | 是 |
item |
当前遍历的元素在循环中的别名 | 是 |
index |
当前遍历的索引(List 为下标,Map 为 Key) | 否 |
open |
循环开始前拼接的字符串(如 () |
否 |
close |
循环结束后拼接的字符串(如 )) |
否 |
separator |
元素之间的分隔符(如 ,) |
否 |
场景一:IN 查询
java
// Mapper 接口
List<Car> selectByIds(@Param("ids") List<Long> ids);
xml
<select id="selectByIds" resultType="car">
SELECT * FROM t_car
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
传入 [1, 2, 3],生成的 SQL:
sql
SELECT * FROM t_car WHERE id IN (1, 2, 3)
场景二:批量插入
xml
<insert id="insertBatch">
INSERT INTO t_car (car_num, brand, guide_price) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.carNum}, #{item.brand}, #{item.guidePrice})
</foreach>
</insert>
collection 的取值规则
| 参数类型 | collection 写法 |
|---|---|
直接传 List(无 @Param) |
list |
直接传 Array(无 @Param) |
array |
传 Set(无 @Param) |
collection |
使用 @Param("ids") |
ids(注解值) |
⚠️ 强烈建议:无论何种集合,都使用 @Param 注解明确命名!
七、变量定义标签
7. <bind> 标签
从 OGNL 表达式创建变量,供 SQL 后续使用。
场景:模糊查询拼接 %
xml
<select id="selectByBrand" resultType="car">
<bind name="pattern" value="'%' + brand + '%'"/>
SELECT * FROM t_car WHERE brand LIKE #{pattern}
</select>
调用时传入 brand = "宝马",pattern 被绑定为 "%宝马%"。
优点 :避免在不同数据库中写不同的 CONCAT 函数,提高了 SQL 的可移植性。
场景:复杂日期计算
xml
<bind name="startDate" value="T(java.time.LocalDate).now().minusDays(7)"/>
SELECT * FROM orders WHERE create_time > #{startDate}
八、底层原理:OGNL 表达式
动态 SQL 的条件判断依赖于 OGNL(Object-Graph Navigation Language,对象图导航语言)。
常用表达式示例
| 表达式 | 说明 |
|---|---|
brand != null |
判断是否为空 |
brand != null and brand != '' |
判断非空且非空字符串 |
age > 18 |
数字比较 |
name == 'admin' |
字符串比较 |
list.size() > 0 |
集合大小判断 |
@com.xie.util.Validator@isValid(brand) |
调用静态方法 |
安全警告
OGNL 表达式在 XML 中直接书写,注意:
and必须小写 (不能写成&&)。- 字符串比较用
==(如'admin')。 - 注意空指针 :
user.name如果user为null,会报错。
九、综合实战:复杂查询示例
xml
<select id="selectByComplexCondition" resultType="car">
SELECT * FROM t_car
<where>
<!-- 多字段模糊查询 -->
<if test="keyword != null and keyword != ''">
AND (brand LIKE CONCAT('%', #{keyword}, '%')
OR car_type LIKE CONCAT('%', #{keyword}, '%'))
</if>
<!-- 分支选择 -->
<choose>
<when test="priceType == 'low'">
AND guide_price < 10
</when>
<when test="priceType == 'mid'">
AND guide_price BETWEEN 10 AND 30
</when>
<when test="priceType == 'high'">
AND guide_price > 30
</when>
</choose>
<!-- 生产日期范围 -->
<if test="startDate != null and endDate != null">
AND produce_time BETWEEN #{startDate} AND #{endDate}
</if>
<!-- 多选过滤 -->
<if test="types != null and types.size() > 0">
AND car_type IN
<foreach collection="types" item="type" open="(" close=")" separator=",">
#{type}
</foreach>
</if>
</where>
<!-- 动态排序 -->
<if test="orderColumn != null and orderColumn != ''">
ORDER BY ${orderColumn}
<if test="orderDirection != null and orderDirection != ''">
${orderDirection}
</if>
</if>
</select>
十、常见易错点与避坑指南
1. AND 和 OR 多余问题
xml
<!-- ❌ 错误:不使用 <where>,第一个条件前有 AND -->
SELECT * FROM t_car
WHERE
<if test="brand != null">
AND brand = #{brand} <!-- 如果这个条件成立,SQL 变成 WHERE AND brand... -->
</if>
<!-- ✅ 正确:使用 <where> -->
SELECT * FROM t_car
<where>
<if test="brand != null">
AND brand = #{brand}
</if>
</where>
2. 空字符串判断
xml
<!-- ❌ 错误:只判 null,不判空字符串 -->
<if test="brand != null">
<!-- ✅ 正确:同时判 null 和空字符串 -->
<if test="brand != null and brand != ''">
3. ${} 注入风险
在 ORDER BY 等必须使用 ${} 的场景,务必对参数进行白名单校验:
java
public String validateOrderColumn(String column) {
Set<String> validColumns = Set.of("id", "brand", "guide_price");
if (!validColumns.contains(column)) {
throw new IllegalArgumentException("非法排序字段");
}
return column;
}
4. collection 写错
java
// Mapper 接口
List<Car> selectByIds(List<Long> ids); // 无 @Param
xml
<!-- ❌ 错误:collection="ids" 但没写 @Param,实际可用的是 list -->
<foreach collection="ids" ...>
<!-- ✅ 正确:使用默认的 list -->
<foreach collection="list" ...>
最佳实践 :一律使用 @Param 注解,一劳永逸。
十一、动态 SQL 最佳实践总结
- 尽量使用
<where>和<set>,避免手动处理AND/OR和逗号。 - 条件判断必须同时检查
null和空字符串 (!= null and != '')。 - 集合遍历务必使用
@Param注解 明确collection名称。 - 动态排序/表名使用
${}时,必须在代码层做白名单校验,防止 SQL 注入。 - 充分利用
<bind>标签提高 SQL 可移植性(如模糊查询)。 - 不要过度使用动态 SQL,过于复杂的动态逻辑建议拆分到 Java 代码中处理。
- 开启 MyBatis 日志 (
STDOUT_LOGGING),便于观察最终生成的 SQL 是否正确。
动态 SQL 是 MyBatis 的灵魂,熟练掌握它,可以让你的数据访问层代码既灵活又安全,从容应对各种复杂的业务查询需求。