MyBatis 动态 SQL 常用标签有哪些?原理是什么?
简化版
MyBatis 动态 SQL 用 if、choose/when/otherwise、where、set、trim、foreach 等节点,根据参数在运行时生成 SQL。判断表达式由 OGNL 计算,最终生成 BoundSql;值仍应使用 #{} 参数化,动态 SQL 不等于可以安全使用 ${} 拼接用户输入。
详细版
where 只在内部有内容时输出 WHERE,并去掉开头多余的 AND/OR;set 为更新语句输出 SET 并移除末尾逗号;trim 可自定义前后缀与需要删除的文本。choose 类似互斥分支,foreach 适合 IN 条件和批量 values,bind 可创建供后续使用的 OGNL 变量。
动态 SQL 在 SqlSource 计算阶段生成 SQL 文本和 ParameterMapping。过多业务规则放在 XML 会难以测试,空集合、全空更新条件和无 WHERE 删除必须显式防护。
完整版教学
一、if 与 where
SELECT id, name, status FROM users
<where>
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
没有条件时 where 不输出,有条件时自动处理前导 AND/OR,避免手工判断第一个条件。它只修整 SQL 片段,不验证查询范围是否符合业务安全要求。
| 标签 | 解决的问题 | 典型场景 |
|---|---|---|
if | 条件满足才拼接片段 | 可选查询条件、可选更新字段 |
choose/when/otherwise | 多个分支只选一个 | id、email、手机号三选一查询 |
where | 自动加 WHERE 并移除开头 AND/OR | 动态查询条件 |
set | 自动加 SET 并移除末尾逗号 | 动态更新字段 |
foreach | 遍历集合生成多个占位符 | IN 查询、批量 values |
trim | 自定义前缀后缀和修剪规则 | 自定义 where/set 形态 |
记忆钩子:动态 SQL 不是“字符串随便拼”,而是 MyBatis 先用节点树生成 SQL 结构,再让
#{}负责参数绑定。
二、choose 表达互斥分支
<choose>
<when test="id != null">WHERE id = #{id}</when>
<when test="email != null">WHERE email = #{email}</when>
<otherwise>WHERE status = 'ACTIVE'</otherwise>
</choose>
choose 只选择第一个满足的 when,否则走 otherwise;多个独立 if 则可能同时拼接。选择哪个取决于条件能否共存。
三、set 防止多余逗号
UPDATE users
<set>
<if test="name != null">name = #{name},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
如果所有更新字段都为空,set 可能没有有效内容,最终 SQL 仍会非法。服务层或 SQL 中应明确禁止“没有任何修改字段”的请求。
四、foreach 展开集合
foreach 可指定 collection、item、index、open、close、separator,为每个元素创建独立上下文和参数映射。除 IN 外,也可生成批量 INSERT 的多组 values。
空集合必须先定义语义:返回空结果、跳过语句还是报参数错误。绝不能因 IN 条件消失而意外执行全表查询或更新。
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
如果 ids = [10, 20, 30],最终 SQL 形态接近 id IN (?, ?, ?),并生成 3 个 ParameterMapping。若 ids 有 2000 个元素,SQL 里就有 2000 个占位符,可能碰到数据库参数个数或 SQL 长度限制,所以大集合要分片或改临时表方案。
五、trim、bind 与 sql/include
trim 是 where 和 set 背后的通用修整能力,可配置 prefix、suffix、prefixOverrides、suffixOverrides。bind 能用 OGNL 预先计算变量,例如构造 LIKE 参数,再通过 #{pattern} 安全绑定。
<sql> 与 <include> 可复用静态或少量动态片段,但复用层级过深会让最终 SQL 难以阅读。排错时应开启合适 SQL 日志或测试 BoundSql,而不是只看碎片文件。
六、动态 SQL 的安全边界
OGNL 条件决定拼不拼某段 SQL,#{} 决定值如何参数化,二者职责不同。表名、排序列等确需文本结构时必须从固定白名单选择,不能把请求参数直接交给 ${}。
删除和更新尤其要设置强制主键/租户条件,并通过测试覆盖所有条件为空的分支。
七、常见误区与追问
- 误区:用了动态 SQL 就可以放心使用
${}。 动态节点只决定片段是否出现,用户值仍应使用#{}参数化,${}只能用于白名单结构片段。 - 误区:
where会保证查询一定安全。where只修剪语法,不会自动防止条件全空导致全表查询。 - 追问:
choose和多个if有什么区别?choose只命中第一个满足分支,多个if可以同时拼接,适合不同业务语义。 - 追问:
set还需要服务层校验吗? 需要,所有字段为空时 SQL 可能非法,或者业务上不应允许空更新。 - 误区:
foreach空集合自然等于查不到。 空集合可能生成非法 SQL,也可能导致条件消失,必须显式定义空集合语义。 - 追问:如何排查动态 SQL 最终生成了什么? 开启 SQL 日志、在测试中检查 BoundSql,关注 SQL 文本、ParameterMapping 和附加参数。
八、加强记忆
if 决定可选条件,choose 选一个分支,where/set/trim 修整语法,foreach 展开集合。动态节点生成 BoundSql,但值仍走 #{};最危险的边界是空集合、空更新字段和 WHERE 条件全部消失。