文章目录
-
- [一、动态 SQL 总述](#一、动态 SQL 总述)
-
- [1.1 什么是动态 SQL?为什么需要它?](#1.1 什么是动态 SQL?为什么需要它?)
- 二、各标签详解
-
- [2.1 `<if>` --- 条件判断标签](#2.1
<if>— 条件判断标签) - [2.2 `<trim>` --- 万能修剪标签](#2.2
<trim>— 万能修剪标签) -
- [场景一:多字段可选插入(用 `suffixOverrides` 去逗号)](#场景一:多字段可选插入(用
suffixOverrides去逗号)) - [场景二:多条件查询(用 `prefixOverrides` 去 AND)](#场景二:多条件查询(用
prefixOverrides去 AND))
- [场景一:多字段可选插入(用 `suffixOverrides` 去逗号)](#场景一:多字段可选插入(用
- [2.3 `<where>` --- 查询专用条件标签](#2.3
<where>— 查询专用条件标签) - [2.4 `<set>` --- 更新专用标签](#2.4
<set>— 更新专用标签) - [2.5 `<foreach>` --- 遍历集合标签](#2.5
<foreach>— 遍历集合标签) - [2.6 `<sql>` + `<include>` --- SQL 片段复用标签](#2.6
<sql>+<include>— SQL 片段复用标签)
- [2.1 `<if>` --- 条件判断标签](#2.1
- 三、补充配套知识点
-
- [3.1 注解版动态 SQL 的限制与解决方案](#3.1 注解版动态 SQL 的限制与解决方案)
- [3.2 逻辑删除配套设计](#3.2 逻辑删除配套设计)
- [3.3 标签选型速查表](#3.3 标签选型速查表)
- [3.4 易错点总结](#3.4 易错点总结)
- 四、全文总结
-
- [4.1 核心价值回顾](#4.1 核心价值回顾)
- [4.2 核心知识点复盘](#4.2 核心知识点复盘)
本文基于 Spring Boot + MyBatis 实战项目,系统讲解 MyBatis 动态 SQL 六大核心标签的用法、原理、选型策略和常见踩坑点。
一、动态 SQL 总述
1.1 什么是动态 SQL?为什么需要它?
先看一个真实场景:后台用户列表页,支持按年龄、性别、状态三个条件组合筛选。如果不用动态 SQL,你需要写出以下所有排列组合:
sql
-- 无条件:查询全部
SELECT * FROM user_info;
-- 仅按年龄查
SELECT * FROM user_info WHERE age = ?;
-- 按年龄 + 性别查
SELECT * FROM user_info WHERE age = ? AND gender = ?;
-- 按年龄 + 性别 + 状态查
SELECT * FROM user_info WHERE age = ? AND gender = ? AND delete_flag = ?;
-- ... 还有 5 种组合未列出
三个条件 = 8 种组合 = 8 条 SQL。五个条件呢?不用动态 SQL,这种 Mapper 代码根本没法维护。
动态 SQL 的核心价值就是:根据传入参数的有无和值的不同,自动拼接生成不同的 SQL 语句 。你只需写一套带判断逻辑的 SQL 模板,MyBatis 在运行期根据实际参数动态组装。MyBatis 为此提供了一整套专用 XML 标签:<if>、<where>、<set>、<trim>、<foreach>、<sql> / <include>。
注意:动态 SQL 标签默认只在 XML 映射文件中使用 。注解方式需额外包裹
<script>标签(见第三章)。
二、各标签详解
2.1 <if> --- 条件判断标签
核心作用:单条件判断------当表达式成立时,才将标签内部的 SQL 片段拼接到最终 SQL 中。它是所有动态 SQL 标签中最基础、使用频率最高的一个。
属性说明:
| 属性 | 说明 |
|---|---|
test |
OGNL 表达式,判断条件是否成立(如 age != null、name != null and name != '') |
基础示例------多条件可选查询(单用 <if>,有问题版):
xml
<!-- ❌ 这个写法有 Bug -->
<select id="searchUser" resultType="com.zmt.mybatisdemo.model.UserInfo">
SELECT * FROM user_info
WHERE
<if test="age != null">age = #{age}</if>
<if test="gender != null">AND gender = #{gender}</if>
<if test="deleteFlag != null">AND delete_flag = #{deleteFlag}</if>
</select>
执行结果分析:
| 传参情况 | 生成的 SQL | 结果 |
|---|---|---|
| 三个参数全传 | WHERE age = ? AND gender = ? AND delete_flag = ? |
✅ 正确 |
只传 age |
WHERE age = ? |
✅ 正确 |
只传 gender |
WHERE AND gender = ? |
❌ 语法错误 :WHERE AND |
| 三个都不传 | WHERE |
❌ 语法错误 :WHERE 后面没条件 |
问题根因:单独的
<if>只负责"是否拼入",不负责"拼入后的 SQL 语法是否合法"。多余的AND前缀和孤零零的WHERE关键字是<if>单独使用时最常见的两个坑。
要解决这个问题,就需要引入 <where> 或 <trim> 标签。
2.2 <trim> --- 万能修剪标签
<trim> 是 MyBatis 动态 SQL 中通用性最强 的标签,<where> 和 <set> 本质上都是 <trim> 的特定简化版。它可以在包裹的 SQL 片段前后添加/移除指定字符串。
四大属性:
| 属性 | 作用 | 常见取值 |
|---|---|---|
prefix |
整个语句块开头添加指定前缀 | WHERE、SET、( |
suffix |
整个语句块末尾添加指定后缀 | ) |
prefixOverrides |
自动移除语句块开头多余的指定字符串 | AND、OR |
suffixOverrides |
自动移除语句块末尾多余的指定字符串 | ,(逗号最常用) |
场景一:多字段可选插入(用 suffixOverrides 去逗号)
这是项目中真实存在的代码(UserInfoMapper.xml):
xml
<insert id="insertUserByCondition">
INSERT INTO user_info
<!-- 字段列表:末尾自动去掉多余的逗号,外层包括号 -->
<trim suffixOverrides="," suffix=")" prefix="(">
<if test="username != null">username,</if>
<if test="password != null">password,</if>
<if test="age != null">age,</if>
<if test="gender != null">gender,</if>
<if test="deleteFlag != null">delete_flag,</if>
</trim>
VALUES
<!-- 值列表:同样逻辑,末尾去逗号 -->
<trim suffixOverrides="," prefix="(" suffix=")">
<if test="username != null">#{username},</if>
<if test="password != null">#{password},</if>
<if test="age != null">#{age},</if>
<if test="gender != null">#{gender},</if>
<if test="deleteFlag != null">#{deleteFlag},</if>
</trim>
</insert>
执行过程推演 :假设只传了 username 和 age 两个字段,gender 和 deleteFlag 为 null:
- 字段
<trim>内:username,+age,→ 末尾逗号被suffixOverrides=","自动删除 → 再加上prefix="("和suffix=")"→ 最终输出:(username, age) - 值
<trim>内:#{username},+#{age},→ 同理 → 最终输出:(#{username}, #{age}) - 最终 SQL:
INSERT INTO user_info (username, age) VALUES (#{username}, #{age})✅ 完美!
场景二:多条件查询(用 prefixOverrides 去 AND)
xml
<select id="searchByCondition" resultType="com.zmt.mybatisdemo.model.UserInfo">
SELECT * FROM user_info
<!-- 至少一条 if 成立时,开头加 WHERE;同时去除最前面的 AND -->
<trim prefix="WHERE" prefixOverrides="AND">
<if test="age != null">age = #{age}</if>
<if test="gender != null">AND gender = #{gender}</if>
<if test="deleteFlag != null">AND delete_flag = #{deleteFlag}</if>
</trim>
</select>
技巧:每个
<if>内部都以AND开头(第一个除外),让prefixOverrides="AND"兜底删除。这样所有<if>的条件写法保持一致,不用纠结哪个是"第一个条件"。
2.3 <where> --- 查询专用条件标签
<where> 是 <trim prefix="WHERE" prefixOverrides="AND | OR"> 的简化版,专为查询设计。
核心行为:
- 内部至少一条
<if>成立时,自动在 SQL 中插入WHERE关键字; - 自动移除第一个成立条件开头 多余的
AND或OR; - 如果所有
<if>都不成立,WHERE关键字也不会出现。
项目实战代码(UserInfoMapper.xml):
xml
<select id="selectUserByCondition" resultType="com.zmt.mybatisdemo.model.UserInfo">
SELECT * FROM user_info
<where>
<if test="age != null">age = #{age}</if>
<if test="deleteFlag != null">AND delete_flag = #{deleteFlag}</if>
</where>
</select>
四种传参情况推演:
| 传入参数 | <where> 处理后生成的 SQL 片段 |
最终完整 SQL |
|---|---|---|
age=18, deleteFlag=0 |
WHERE age = ? AND delete_flag = ? |
SELECT * FROM user_info WHERE age = ? AND delete_flag = ? |
age=18, deleteFlag=null |
WHERE age = ? |
SELECT * FROM user_info WHERE age = ? |
age=null, deleteFlag=0 |
WHERE delete_flag = ?(前面的 AND 被自动移除) |
SELECT * FROM user_info WHERE delete_flag = ? |
age=null, deleteFlag=null |
空字符串(连 WHERE 也不输出) | SELECT * FROM user_info |
⚠️ 局限 :
<where>只能去掉开头 的AND/OR,无法处理末尾多余的逗号 。所以更新场景请用<set>或<trim>。
反面教材------WHERE 1=1 写法:
xml
<!-- ❌ 不推荐:用 WHERE 1=1 规避 AND 问题,SQL 丑陋且让索引优化器困惑 -->
SELECT * FROM user_info WHERE 1=1
<if test="age != null">AND age = #{age}</if>
<if test="gender != null">AND gender = #{gender}</if>
能用 <where> 就不要写 WHERE 1=1 。虽然功能上没问题,但它透露着一种"我不知道有 <where> 标签"的信号。
2.4 <set> --- 更新专用标签
<set> 是 <trim prefix="SET" suffixOverrides=","> 的简化版,专为 UPDATE 语句设计。
核心行为:
- 自动生成
SET关键字; - 自动剔除最后一个
<if>拼接后末尾多余的逗号,。
项目实战代码(UserInfoMapper.xml):
xml
<update id="updateUserByCondition">
UPDATE user_info
<set>
<if test="gender != null">gender = #{gender},</if>
<if test="deleteFlag != null">delete_flag = #{deleteFlag},</if>
</set>
WHERE id = #{id}
</update>
推演 :假设只传了 gender=1,deleteFlag=null:
<set>内部:只有gender = #{gender},成立;suffixOverrides=","自动去除末尾逗号 →gender = #{gender};- 加上
SET前缀 →SET gender = #{gender}; - 最终 SQL:
UPDATE user_info SET gender = ? WHERE id = ?✅
调用示例(项目测试代码):
java
@Test
void updateUserByCondition() {
UserInfo userInfo = new UserInfo();
userInfo.setId(6);
userInfo.setGender(1);
userInfo.setDeleteFlag(1);
// 只 set 了 gender 和 deleteFlag,其他字段为 null 不会更新
Integer rows = userInfoMapperXML.updateUserByCondition(userInfo);
System.out.println("更新 " + rows + " 条");
}
典型使用场景:前端提交一个"编辑用户"表单,用户只改了性别和状态,后端只更新这两个字段,不会把没传的字段误更新为 null。
2.5 <foreach> --- 遍历集合标签
用于遍历数组、List、Set、Map 等集合,是批量操作的利器。
核心属性:
| 属性 | 说明 | 示例值 |
|---|---|---|
collection |
绑定的集合参数名(List 写 list,数组写 array,@Param 写注解别名) |
ids、list、array |
item |
遍历过程中每个元素的别名 | id、user |
open |
整个遍历结果开头拼接的字符串 | ( |
close |
整个遍历结果结尾拼接的字符串 | ) |
separator |
每两个遍历项之间的分隔符 | , |
index |
遍历的索引(可选,Map 时为 key) | i |
项目实战------批量删除(UserInfoMapper.xml):
xml
<delete id="batchDelete">
DELETE FROM user_info WHERE id IN
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</delete>
Mapper 接口:
java
Integer batchDelete(List<Integer> ids);
调用测试:
java
@Test
void batchDelete() {
List<Integer> ids = List.of(14, 15);
Integer rows = userInfoMapperXML.batchDelete(ids);
System.out.println("删除 " + rows + " 条");
}
生成 SQL 推演 :传入 ids = [14, 15],<foreach> 遍历 2 次,separator="," 在两次之间插入逗号,open="(" + close=")" 包裹 → (14, 15)。最终 SQL:
sql
DELETE FROM user_info WHERE id IN (14, 15)
更多场景示例:
xml
<!-- 批量新增 -->
<insert id="batchInsert">
INSERT INTO user_info (username, password, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.password}, #{user.age})
</foreach>
</insert>
<!-- 多值 OR 查询 -->
<select id="selectByIds" resultType="com.zmt.mybatisdemo.model.UserInfo">
SELECT * FROM user_info
WHERE id IN
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</select>
易错提醒 :
collection的值必须和 Mapper 接口入参对应!
- 方法参数是
List<Integer> ids且没有@Param→collection="list"- 方法参数是
Integer[] arr且没有@Param→collection="array"- 方法参数是
@Param("ids") List<Integer> ids→collection="ids"(推荐写法,明确且不依赖类型默认名)
2.6 <sql> + <include> --- SQL 片段复用标签
当多个查询语句重复书写相同的字段列表时,<sql> + <include> 提供了"定义一次、全局复用"的能力。
定义公共片段:
xml
<!-- 定义公共字段列表,id 全局唯一 -->
<sql id="Base_Column_List">
id, username, password, age, gender, phone,
delete_flag, create_time, update_time
</sql>
引用公共片段:
xml
<!-- 方式一:直接引用全部字段 -->
<select id="selectById" resultType="com.zmt.mybatisdemo.model.UserInfo">
SELECT <include refid="Base_Column_List"/>
FROM user_info
WHERE id = #{id}
</select>
<!-- 方式二:引用字段列表,拼接额外字段 -->
<select id="selectWithExtra" resultType="com.zmt.mybatisdemo.model.UserInfo">
SELECT <include refid="Base_Column_List"/>, extra_field
FROM user_info
</select>
项目中也有类似的 <sql> 用法(UserInfoMapper.xml):
xml
<!-- 将整条基础 SQL 定义为可复用的 sql 片段 -->
<sql id="sql">
SELECT * FROM `user_info`
</sql>
<!-- 在查询方法中直接引用 -->
<select id="selectAll" resultType="com.zmt.mybatisdemo.model.UserInfo">
<include refid="sql"/>
</select>
为什么需要
<sql>复用? 假设数据库user_info表新增了一个<sql>封装后,只要改一处,所有<include>引用处自动生效。
三、补充配套知识点
3.1 注解版动态 SQL 的限制与解决方案
前两篇文章提到了注解式和 XML 式两种开发方式。动态 SQL 标签(<if>、<where> 等)默认只能在 XML 文件中使用 。如果你坚持用注解写动态 SQL,必须将整个动态 SQL 包裹在 <script> 标签内:
java
// ✅ 注解版动态 SQL:必须用 <script> 包裹
@Update("<script>" +
"UPDATE user_info" +
" <set>" +
" <if test='gender != null'>gender = #{gender},</if>" +
" <if test='deleteFlag != null'>delete_flag = #{deleteFlag},</if>" +
" </set>" +
" WHERE id = #{id}" +
"</script>")
Integer updateUserByCondition(UserInfo userInfo);
这种写法的问题:
- 字符串拼接维护困难,IDE 不提供 XML 标签的语法提示和校验;
- SQL 稍复杂时,Java 字符串拼接 + XML 标签混在一起,可读性极差;
- 无法享受 MyBatis XML 插件的跳转、校验、格式化功能。
结论:动态 SQL 场景是决定"必须用 XML"的最后一根稻草。一旦项目中出现动态 SQL 需求,就不要再犹豫是否用注解了,直接切 XML。
3.2 逻辑删除配套设计
生产环境中,逻辑删除 远比物理删除更常见。设计很简单:
① 数据表加字段:
sql
ALTER TABLE user_info ADD COLUMN delete_flag TINYINT DEFAULT 0 COMMENT '0=正常, 1=已删除';
② 所有查询默认过滤已删除数据 (配合 <sql> 复用):
xml
<!-- 公共的"未删除"过滤条件 -->
<sql id="NotDeleted">
AND delete_flag = 0
</sql>
<select id="selectAll" resultType="com.zmt.mybatisdemo.model.UserInfo">
SELECT * FROM user_info
WHERE 1=1
<include refid="NotDeleted"/>
</select>
③ "删除"操作改为 UPDATE:
java
// 逻辑删除:将 delete_flag 从 0 改为 1,而不是 DELETE FROM
@Update("UPDATE user_info SET delete_flag = 1 WHERE id = #{id}")
Integer deleteLogically(@Param("id") Integer id);
逻辑删除 vs 物理删除:
| 维度 | 逻辑删除 | 物理删除 |
|---|---|---|
| 实现方式 | UPDATE SET delete_flag=1 |
DELETE FROM |
| 数据留存 | ✅ 数据仍在,可查历史、可恢复 | ❌ 数据永久消失 |
| 误删恢复 | ✅ 改回 delete_flag=0 即可 |
❌ 只能从备份恢复 |
| 查询成本 | 每次查询要多加 AND delete_flag=0 |
无额外条件 |
| 使用场景 | C 端业务数据的默认方案 | 临时数据、日志清理、极特殊合规要求 |
3.3 标签选型速查表
| 需求场景 | 推荐标签组合 | 一句话理由 |
|---|---|---|
| 多条件分页 / 模糊查询 | <where> + <if> |
<where> 自动去 AND 前缀,专为查询设计 |
| 动态选择性更新字段 | <set> + <if> |
<set> 自动去末尾逗号,专为 UPDATE 设计 |
| 多字段可选插入 / 自定义前后缀 | <trim> + <if> |
万能标签,prefix/suffix/prefixOverrides/suffixOverrides 全支持 |
| 批量删除 / 批量新增 / IN 查询 | <foreach> |
遍历集合生成 (1,2,3) 或多条 VALUES |
| 重复字段列表 / 公共 SQL 片段 | <sql> + <include> |
定义一次全局复用,字段变更只改一处 |
3.4 易错点总结
① 只用 <if> 不用 <where> / <trim>------多余 AND 报错
xml
<!-- ❌ 当第一个 <if> 不成立、第二个成立时,SQL 会变成 WHERE AND xx -->
SELECT * FROM user_info WHERE
<if test="age != null">age = #{age}</if>
<if test="gender != null">AND gender = #{gender}</if>
修复 :用 <where> 包裹,或在前面的 <if> 条件末尾加 AND 配合 <trim prefixOverrides="AND">。
② 更新语句只用 <if> 不用 <set>------末尾逗号报错
xml
<!-- ❌ 如果 deleteFlag 为 null,SQL 变成 SET gender = ?,(多一个逗号) -->
UPDATE user_info SET
<if test="gender != null">gender = #{gender},</if>
<if test="deleteFlag != null">delete_flag = #{deleteFlag},</if>
修复 :用 <set> 包裹,<set> 会自动去掉最后一个逗号。
③ <foreach> 的 collection 名写错------集合找不到
java
// 接口方法
Integer batchDelete(@Param("ids") List<Integer> ids);
xml
<!-- ✅ 正确:collection 写 @Param 的别名 "ids" -->
<foreach collection="ids" ...>
<!-- ❌ 错误:写了 "list",但方法上有 @Param("ids") -->
<foreach collection="list" ...>
规则:有
@Param→collection写@Param的值;无@Param→ List 写list,数组写array。
四、全文总结
4.1 核心价值回顾
动态 SQL 是 MyBatis 区别于"手写 JDBC"和"简单 ORM"的核心竞争力。它让你用一套 SQL 模板覆盖所有参数组合,避免了手动拼接 SQL 的重复劳动和潜在的注入风险。
六组标签的记忆口诀:
| 标签 | 口诀 |
|---|---|
<if> |
有就拼,没有就跳过 |
<where> |
自动加 WHERE,自动去 AND |
<set> |
自动加 SET,自动去逗号 |
<trim> |
前后缀万能补刀,<where> 和 <set> 都是我的简化版 |
<foreach> |
遍历集合,首尾包裹,中间分隔 |
<sql> + <include> |
定义一处,到处复用 |
4.2 核心知识点复盘
| 知识模块 | 要点提炼 |
|---|---|
<if> |
test 属性写 OGNL 表达式,单用容易出 AND/逗号/空 WHERE 语法问题 |
<trim> |
四种属性覆盖增删改查全场景;prefixOverrides 去 AND,suffixOverrides 去逗号 |
<where> |
只用于查询;仅内部有成立条件时才输出 WHERE;自动去开头 AND/OR |
<set> |
只用于更新;自动去末尾逗号 |
<foreach> |
collection 名与 @Param 保持同步;open/close/separator 控制输出格式 |
<sql> + <include> |
字段变更只改一处,全项目自动生效 |
| 注解动态 SQL | 需 <script> 包裹,可读性差,复杂场景不推荐 |
| 逻辑删除 | 加 delete_flag 字段,删改 UPDATE,查加过滤条件 |