一、动态SQL
动态SQL是Mybatis的强⼤特性之⼀,能够完成不同条件下不同的sql拼接
可以参考官⽅⽂档:Mybatis动态SQL
1.1动态 SQL 的<if>标签用法
(1)为什么要用动态 SQL?
在业务场景中(比如注册用户),存在必填字段(比如用户名、密码)和非必填字段(比如性别、生日)。如果直接写固定 SQL,非必填字段没传值时,会出现两种问题:
- 要么 SQL 语法错误(比如多了逗号);
- 要么把数据库字段的默认值覆盖成
null(如果字段有默认值的话)。
所以 MyBatis 的动态 SQL 可以根据参数是否存在,自动拼接 / 忽略对应的 SQL 片段。
(2)<if>标签的核心逻辑
<if test="条件">...</if>表示:当test里的条件满足时,才会把<if>包裹的 SQL 片段拼接到最终 SQL 中。
以 “注册用户” 为例,gender是非必填字段,所以用<if>判断:
- 当传入的
userInfo对象中gender != null时,才拼接gender对应的字段和值; - 当
gender == null时,自动忽略这部分 SQL。
(3)代码示例拆解
以 “插入用户” 的insertUserByCondition为例:
1. 接口定义
这里传入的UserInfo是包含所有字段(必填 + 非必填)的实体类。
2. Mapper.xml 中的<if>用法
当gender有值:
最终拼接的 SQL 是:
当gender为null:
最终拼接的 SQL 自动忽略gender相关片段:
3. 注解方式(不推荐)
也可以用@Insert注解 +<script>标签写动态 SQL,但可读性差,所以一般不用:
关键注意点
test里的表达式是 “实体类属性”,不是数据库字段:比如test="gender != null"中的gender是UserInfo类的属性名,不是数据库表的列名。为什么不能直接写
null?如果不判断,直接写gender字段,当传值为null时:- 若数据库中
gender字段有默认值(比如默认 “未知”),会被null覆盖; - 若字段允许
null,虽然语法没问题,但不符合 “非必填则用默认值” 的业务逻辑。
- 若数据库中
- 列名部分的
<if>判断哪个字段要插入,VALUES 值部分就必须用完全相同的<if>判断(否则会出现 “列名和值数量不匹配” 的 SQL 语法错误)。
1.2动态SQL的<trim>标签用法
(1)<trim>标签的作用
当需要动态生成多个字段 / 条件时(比如插入语句的列名、VALUES 值,或者查询的 WHERE 条件),<trim>可以通过属性自动处理 “前缀 / 后缀” 和 “多余符号”,避免手动拼接 SQL 的冗余问题。
(2)<trim>的 4 个核心属性
| 属性名 | 作用 |
|---|---|
prefix | 给整个语句块添加一个前缀(比如给列名块加(,给 VALUES 加() |
suffix | 给整个语句块添加一个后缀(比如给列名块加),给 VALUES 加)) |
prefixOverrides | 自动去掉语句块开头的指定符号(比如多余的AND/OR) |
suffixOverrides | 自动去掉语句块结尾的指定符号(比如多余的逗号,) |
(3)案例:动态插入语句的<trim>用法
以 “插入用户信息(多字段动态可选)” 为例,解析<trim>的实际作用:
1. 动态生成 “插入列名”
2.只传 age+phone(username/password 没传)
拼接后 SQL:
- 适配 “只补全用户联系方式” 的业务场景,不用强制传用户名密码;
- 避免了 “没传的字段被插入 NULL” 的问题。
trim+if 动态插入的核心规则:
- 判断一致:列名
<if>判断哪个字段要插入 → VALUES<if>必须用完全相同的 test 条件(字段名、非空判断都一致),保证 “列数 = 值数”; - 符号统一:列名
<trim>用了什么prefix/suffix(比如())→ VALUES<trim>也用完全一样的(保证 SQL 语法对称); - 逗号兜底:所有
<if>结尾都加,→ 统一靠trim的suffixOverrides=","去掉最后一个冗余逗号(不用手动判断 “最后一个字段要不要加逗号”)。
反例(踩坑点)
如果违背这个规则,比如:
- 列名判断
username != null,VALUES 判断userName != null(大小写不一致)→ 可能导致列名有但值无,SQL 报错; - 列名
<trim>加(),VALUES<trim>漏加 → 生成INSERT INTO user_info (username) VALUES #{username}(语法错误); - 手动控制逗号(比如最后一个
<if>不加,)→ 字段顺序变动时,容易少逗号 / 多逗号。
拓展:trim 不止用于插入,还能用于查询
比如动态 WHERE 条件(去掉多余的 AND/OR):
- 逻辑:
prefix="WHERE"加前缀,prefixOverrides="AND | OR"去掉开头多余的 AND/OR; - 最终生成:
SELECT * FROM user_info WHERE username = #{username} AND age = #{age}。
简单说,只要是 “动态拼接 SQL 块,需要补前后缀 / 清冗余符号”,trim+if 都是最优解,核心就是 “判断对称、符号对称、冗余交给 trim 处理”。
1.3动态SQL的<where>标签用法
(1)<where>标签的作用
当需要动态组装查询条件(比如根据入参非空情况拼接 WHERE 子句)时,<where>可以自动处理两个核心问题:
- 自动补 WHERE 关键词:只有当子元素(比如
<if>)有内容时,才会插入WHERE; - 自动去冗余 AND/OR:如果条件块开头是
AND或OR,会自动去掉(避免 SQL 语法错误)。
(2)案例:动态查询条件的实现
以 “根据用户对象的非空属性查询” 为例,看<where>的实际效果:
原始需求
- 入参
UserInfo对象中,非 null 的属性作为查询条件; - 比如入参
age=18、gender=1,则查询条件为WHERE age=18 AND gender=1。
代码逻辑(Mapper.xml)
(3)不同场景下的最终 SQL 效果
场景 1:入参age=18、gender=1(两个条件非空)
MyBatis 解析后生成的 SQL:
<where>的作用:- 子元素有内容 → 插入
WHERE; - 第一个条件开头的
and→ 自动去掉; - 最终条件块为
WHERE age=18 AND gender=1(语法正确)。
- 子元素有内容 → 插入
场景 2:入参只有deleteFlag=0(一个条件非空)
最终生成的 SQL:
<where>的作用:去掉条件开头的and,插入WHERE。
场景 3:入参所有属性为 null(无查询条件)
最终生成的 SQL:
<where>的作用:子元素无内容 → 不插入WHERE(避免生成WHERE后无条件的语法错误)。
(4)<where>与<trim>的关系
<where>本质是<trim>的 “语法糖”,可以用<trim>手动实现相同逻辑:
但<trim>有个小缺点:当子元素无内容时,会保留WHERE关键词(比如场景 3 会生成from userinfo WHERE,导致 SQL 语法错误)。
因此,<where>是动态 WHERE 条件的更优选择(自动处理空条件 + 去冗余 AND/OR)。
核心总结
<where>的价值是 “智能拼接 WHERE 条件”:
- 有条件 → 补
WHERE+ 去开头的 AND/OR; - 无条件 → 不补
WHERE,避免语法错误; - 替代手动拼接,让动态查询更简洁、安全。
| 维度 | <where>标签 | <trim>标签 | |
|---|---|---|---|
| 核心定位 | 专门解决「动态 WHERE 条件」拼接问题 | 通用型标签,解决各类 SQL 块的拼接问题(插入 / 查询 / 更新) | |
| 自动补前缀 | 仅补WHERE(仅当子元素有内容时) | 可自定义前缀(如WHERE/SET/(等) | |
| 自动去冗余符号 | 仅去掉开头的AND/OR | 可自定义去掉「开头 / 结尾」的任意符号(如AND/,/OR等) | |
| 空条件处理 | 子元素无内容 → 不生成WHERE(无语法错误) | 子元素无内容 → 仍会生成自定义前缀(如prefix="WHERE"会导致WHERE无后续条件,报错) | |
等价<trim>写法 | `<trim prefix="WHERE" prefixOverrides="AND | OR">` | 无固定等价写法,按需配置prefix/suffix/prefixOverrides/suffixOverrides |
| 典型使用场景 | 动态查询的 WHERE 条件拼接(最常用) | 1. 动态插入的列名 / VALUES 拼接(去结尾逗号)2. 动态更新的 SET 子句(去结尾逗号)3. 自定义复杂拼接规则 | |
| 优势 | 专为 WHERE 条件设计,无需手动配置,更简洁 | 通用性强,适配所有动态 SQL 拼接场景 | |
| 劣势 | 功能单一,仅能处理 WHERE 条件 | 需手动配置属性,空条件场景需额外判断 |
- 优先用
<where>的场景:只要是「动态组装 WHERE 查询条件」,优先选<where>(无需配置属性,自动处理空条件 + 冗余 AND/OR,不易出错)。 - 必须用
<trim>的场景:- 动态插入(拼接列名 / VALUES,去结尾逗号);
- 动态更新(拼接 SET 子句,如
<trim prefix="SET" suffixOverrides=",">); - 需要自定义前缀 / 后缀 / 冗余符号(比如拼接 IN 条件的括号)。
- 避坑点:用
<trim>模拟<where>时,一定要加额外判断(比如<if>兜底),避免子元素无内容时生成WHERE关键词导致 SQL 报错。
示例:<trim>模拟<where>并兜底(避免空条件)
1.4动态SQL的<set>标签用法
(1)<set>标签的作用
在动态UPDATE语句中,<set>可以自动处理两个关键问题:
- 自动补
SET关键词:仅当子元素(比如<if>)有内容时,才插入SET; - 自动删冗余逗号:如果更新字段列表的结尾有多余的逗号(
,),会自动去掉(避免 SQL 语法错误)。
(2)案例:动态更新用户数据
以 “根据用户对象非空属性更新数据(ID 为更新条件)” 为例,看<set>的实际效果:
需求
- 入参
UserInfo对象中,非 null 的属性作为更新字段; - 必须传入
id(定位要更新的用户); - 比如入参
username="newName"、age=20,则更新 SQL 为update userinfo set username='newName', age=20 where id=?。
代码逻辑(Mapper.xml)
(3)不同场景下的最终 SQL 效果
场景 1:入参username="newName"、age=20(两个字段非空)
生成的 SQL:
<set>的作用:- 子元素有内容 → 插入
SET; - 去掉字段列表结尾的冗余逗号(第二个字段
age=20后的逗号被自动删除)。
- 子元素有内容 → 插入
场景 2:入参只有deleteFlag=1(一个字段非空)
生成的 SQL:
<set>的作用:去掉delete_flag=1后的逗号,避免生成set delete_flag=1,的语法错误。
场景 3:入参只有id(无更新字段,其他属性为 null)
生成的 SQL:
- 注意:这种情况会触发 SQL 语法错误(
update后无SET子句),实际开发中需要加兜底判断(比如确保至少有一个更新字段非空)。
(4)<set>与<trim>的关系
<set>是<trim>的 “语法糖”,可以用<trim>手动实现相同逻辑:
prefix="set"→ 补SET关键词;suffixOverrides=","→ 去掉结尾的冗余逗号。
但<set>是专门为UPDATE语句设计的,写法更简洁,优先推荐使用。
(5)核心总结
<set>的价值是 “智能拼接 UPDATE 的 SET 子句”:
- 有更新字段 → 补
SET+ 去结尾逗号; - 写法简洁,避免手动控制逗号导致的语法错误;
- 实际开发中需加 “至少一个更新字段非空” 的判断,避免场景 3 的语法错误。
1.5动态SQL的<foreach>标签用法
(1)<foreach>的核心属性
| 属性名 | 作用 |
|---|---|
collection | 绑定方法入参的集合 / 数组(如List/Set/ 数组,需与方法参数名一致) |
item | 遍历过程中,集合的每个元素的别名(用于 SQL 中引用) |
open | 遍历结果的开头拼接的字符串(如IN条件的左括号() |
close | 遍历结果的结尾拼接的字符串(如IN条件的右括号)) |
separator | 遍历元素之间的分隔符(如IN条件的逗号,) |
(2)案例:批量删除用户(根据多个 ID)
以 “根据List<Integer> ids删除多个用户” 为例,解析<foreach>的效果:
接口方法
Mapper.xml 代码
(3)最终 SQL 效果
若入参ids = [1,2,3],<foreach>会动态拼接为:
open="("→ 开头加(;close=")"→ 结尾加);separator=","→ 元素间加逗号;item="id"→ 每个元素用#{id}引用。
(4)<foreach>的其他常见场景
除了IN条件,<foreach>还常用于批量插入:
若入参是包含 2 个用户的List,生成 SQL:
(5)核心总结
<foreach>是 MyBatis 实现集合 / 数组动态拼接 SQL的核心标签,通过collection/item/open/close/separator的组合,可灵活适配IN条件、批量操作等场景,避免手动拼接集合元素的繁琐与错误。
1.6动态SQL的<include>标签的用法
(1)问题背景:SQL 片段重复的痛点
在 Mapper.xml 中,多个 SQL 语句常包含重复的片段(比如查询时的字段列表、通用条件),若直接重复编写:
- 代码冗余,维护成本高(修改字段需改所有 SQL);
- 易出错(不同 SQL 中字段列表不一致)。
(2)核心标签:<sql>+<include>
通过 **<sql>定义可复用片段 ** +<include>引用片段,实现 SQL 代码的复用:
| 标签 | 作用 | 核心属性 |
|---|---|---|
<sql> | 封装可复用的 SQL 片段 | id:片段唯一标识 |
<include> | 引用已定义的<sql>片段 | refid:对应<sql>的id |
(3)案例:复用查询字段列表
以 “多个查询 SQL 复用相同的字段列表” 为例:
1. 定义 SQL 片段(用<sql>)
2. 引用 SQL 片段(用<include>)
将原来重复的字段列表替换为<include>:
(4)效果与优势
- 代码复用:重复的字段列表只需写一次,减少冗余;
- 维护便捷:若字段有变更(比如新增
email字段),只需修改<sql id="allColumn">中的内容,所有引用该片段的 SQL 都会自动更新; - 一致性:确保所有查询 SQL 的字段列表完全一致,避免漏写 / 错写字段。
(5)拓展场景
<sql>+<include>不仅可复用字段列表,还能复用通用条件(比如逻辑删除的delete_flag=0):
(6)核心总结
<include>+<sql>是 MyBatis 中SQL 代码复用的核心方案,通过 “定义片段 + 引用片段” 的方式,解决了重复 SQL 片段的冗余与维护问题,是企业级开发中提升代码可维护性的常用技巧。