一、为什么需要动态 SQL?
在实际开发中,我们几乎不可能写出"一成不变"的 SQL。最典型的场景就是多条件组合查询------用户可能只填了姓名,也可能同时填了姓名、类型和状态,还可能什么都不填。
如果用传统 JDBC 拼接字符串,你会写出这样的代码:
java
StringBuilder sql = new StringBuilder("select * from employee where 1=1");
if (name != null) {
sql.append(" and name like '%").append(name).append("%'");
}
if (status != null) {
sql.append(" and status = ").append(status);
}
// ... 一堆 if-else,容易拼错、容易 SQL 注入
痛点很明显:
- 拼接易出错:少一个空格、多一个逗号,SQL 就废了
- SQL 注入风险:手动拼字符串无法使用预编译参数
- 可读性差:业务逻辑和 SQL 拼接混在一起,代码像面条
MyBatis 的动态 SQL 机制就是为了优雅地解决这些问题而诞生的。
二、动态 SQL 的本质
MyBatis 动态 SQL 的底层原理是 OGNL 表达式 + XML 标签解析 。MyBatis 在解析 Mapper XML 时,会将这些标签转换为 SqlNode 对象树,运行时根据传入参数的值,动态地裁剪和拼接 SQL 片段,最终生成一条完整的、可执行的预编译 SQL。
简单理解:动态 SQL = 在 XML 里写 if/else,MyBatis 帮你在运行时智能拼 SQL。
三、九大核心标签全解析
3.1 <if> ------ 条件判断,最基础的守门员
<if> 是使用频率最高的动态 SQL 标签,用于根据参数值决定是否拼接某段 SQL。
XML
<select id="pageQuery" resultType="Employee">
select * from employee
<where>
<if test="name != null and name != ''">
and name like concat('%', #{name}, '%')
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
order by create_time desc
</select>
要点:
test属性使用 OGNL 表达式,支持!=、==、and、or等逻辑运算- 字符串类型建议同时判断
!= null和!= '',否则空字符串会拼出无效条件 <if>体内的 SQL 片段可以包含and/or前缀,配合<where>使用可以自动去除
3.2 <where> ------ 智能处理 WHERE 子句
<where> 标签会做两件聪明的事:
- 当内部有条件满足时,自动添加
WHERE关键字 - 自动去除条件开头多余的
AND或OR
XML
<!-- 如果 name 和 status 都为 null,生成:select * from employee -->
<!-- 如果只有 name 不为 null,生成:select * from employee WHERE name like ... -->
<!-- 不会出现 "WHERE and name like ..." 的尴尬 -->
<select id="list" resultType="Employee">
select * from employee
<where>
<if test="name != null">
and name like concat('%', #{name}, '%')
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>
注意: <where> 只会去除开头 的多余 AND/OR,不会处理中间和结尾的。所以 <if> 里的条件统一以 and 开头是最佳实践。
3.3 <set> ------ 更新操作的好搭档
更新操作最头疼的问题是:用户只改了名字,你总不能把其他字段都更新为 null 吧?<set> 标签专门解决这个问题。
XML
<update id="update" parameterType="Employee">
update employee
<set>
<if test="name != null">name = #{name},</if>
<if test="username != null">username = #{username},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="sex != null">sex = #{sex},</if>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
</update>
<set> 标签的作用:
- 自动在内容前添加
SET关键字 - 自动去除最后一个多余的逗号
,
这就是为什么每个 <if> 里末尾都写了逗号,但不用担心 SQL 报错。
3.4 <choose>/<when>/<otherwise> ------ 多选一的条件分支
<if> 是"满足就拼",但有时候你需要的是"只选一个"的逻辑,类似 Java 的 switch-case。
XML
<select id="listByPriority" resultType="Employee">
select * from employee
<where>
<choose>
<when test="id != null">
id = #{id}
</when>
<when test="name != null and name != ''">
name like concat('%', #{name}, '%')
</when>
<otherwise>
status = 1
</otherwise>
</choose>
</where>
</select>
执行逻辑:
- 按顺序判断
<when>,一旦有满足的就使用,后面的不再判断 - 如果所有
<when>都不满足,执行<otherwise> <otherwise>可以省略
适用场景: 优先级查询、互斥条件选择。
3.5 <foreach> ------ 批量操作之王
当你需要处理 IN 查询或批量插入时,<foreach> 是不可或缺的。
IN 查询
XML
<select id="getByIds" resultType="Category">
select * from category
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
生成效果: select * from category where id in (1, 2, 3)
批量插入
XML
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value)
values
<foreach collection="flavors" item="flavor" separator=",">
(#{flavor.dishId}, #{flavor.name}, #{flavor.value})
</foreach>
</insert>
生成效果:
sql
insert into dish_flavor (dish_id, name, value) values (1,'辣度','微辣'), (1,'甜度','半糖')
属性说明:
| 属性 | 说明 |
|---|---|
collection |
集合参数名,对应接口方法的 @Param 值或参数属性名 |
item |
迭代变量名,在 #{} 中引用 |
index |
索引变量名(List 为下标,Map 为 key) |
open |
整个循环体前添加的字符串 |
close |
整个循环体后添加的字符串 |
separator |
每次迭代之间的分隔符 |
⚠️ 注意: 批量插入时,MySQL 对 SQL 长度有限制(max_allowed_packet,默认 4MB),数据量特别大时建议分批插入。
3.6 <trim> ------ 万能裁剪器
<where> 和 <set> 本质上都是 <trim> 的语法糖:
XML
<!-- where 等价于 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
<!-- set 等价于 -->
<trim prefix="SET" suffixOverrides=",">
...
</trim>
<trim> 的四个属性:
| 属性 | 说明 |
|---|---|
prefix |
给内容整体添加的前缀 |
suffix |
给内容整体添加的后缀 |
prefixOverrides |
去除内容开头指定的字符串 |
suffixOverrides |
去除内容末尾指定的字符串 |
自定义 trim 示例:
XML
<select id="queryByCondition" resultType="Employee">
select * from employee
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">
and name like concat('%', #{name}, '%')
</if>
<if test="status != null">
and status = #{status}
</if>
</trim>
</select>
当只需要 <where> 和 <set> 的能力时,直接用它们更简洁;遇到更复杂的裁剪需求时,<trim> 才上场。
3.7 <sql> / <include> ------ SQL 片段复用
当你发现多个查询中重复出现相同的字段列表或条件片段时,可以用 <sql> 提取公共部分,用 <include> 引用。
XML
<!-- 定义公共字段 -->
<sql id="employeeColumns">
id, name, username, phone, sex, id_number, status, create_time, update_time
</sql>
<!-- 定义公共查询条件 -->
<sql id="employeeCondition">
<if test="name != null and name != ''">
and name like concat('%', #{name}, '%')
</if>
<if test="status != null">
and status = #{status}
</if>
</sql>
<!-- 引用 -->
<select id="pageQuery" resultType="Employee">
select <include refid="employeeColumns"/> from employee
<where>
<include refid="employeeCondition"/>
</where>
order by create_time desc
</select>
<select id="list" resultType="Employee">
select <include refid="employeeColumns"/> from employee
<where>
<include refid="employeeCondition"/>
</where>
</select>
3.8 <bind> ------ 变量绑定
<bind> 允许你在 SQL 中创建一个变量,对 OGNL 表达式的结果进行预处理。
XML
<select id="fuzzyQuery" resultType="Employee">
<bind name="pattern" value="'%' + name + '%'" />
select * from employee
where name like #{pattern}
</select>
典型应用场景:
- 模糊查询的通配符拼接(避免在每个数据库方言中写不同的 concat)
- 对参数做预处理(如大小写转换、字符串截取等)
XML
<!-- 大小写不敏感查询 -->
<select id="search" resultType="Employee">
<bind name="lowerName" value="name.toLowerCase()" />
select * from employee
where LOWER(name) like CONCAT('%', #{lowerName}, '%')
</select>
四、动态 SQL 的性能考量
4.1 SQL 缓存与动态 SQL
MyBatis 有一个重要的内部机制:每个查询都会生成一个 MappedStatement,其中包含 SQL 的解析模板。动态 SQL 的条件分支不会在启动时就固定,而是在每次执行时根据参数重新解析。
但这并不意味着严重的性能问题,因为 MyBatis 内部对 SQL 解析做了缓存优化------相同参数组合生成的 SQL 会被缓存,不会每次都重新解析 OGNL。
4.2 批量操作的选择
| 方案 | 优点 | 缺点 |
|---|---|---|
<foreach> 拼接 |
一次 SQL 完成 | SQL 过长可能超限 |
| Batch Executor | 安全,无 SQL 长度限制 | 多次网络交互 |
| 分批 foreach | 兼顾效率和安全 | 需要手动分批 |
推荐: 数据量小(< 500 条)用 <foreach>;数据量大用 MyBatis 的 BatchExecutor 或分批插入。
五、一张图总结
XML
动态 SQL 标签速查
├── 条件判断
│ ├── <if> → 满足条件就拼接
│ └── <choose> → 多选一(when/otherwise)
├── 子句修饰
│ ├── <where> → 自动加 WHERE,去开头 AND/OR
│ ├── <set> → 自动加 SET,去末尾逗号
│ └── <trim> → 自定义前后缀裁剪(where/set 的底层实现)
├── 集合迭代
│ └── <foreach> → IN 查询、批量插入
├── 复用与绑定
│ ├── <sql>/<include>→ SQL 片段定义与引用
│ └── <bind> → OGNL 变量绑定
└── 注解方式
└── <script> → 注解中写动态 SQL
结语
动态 SQL 是 MyBatis 最核心的特性之一,掌握它不仅是"会用几个标签",更关键的是理解它的设计哲学------将 SQL 的静态结构与运行时的动态条件解耦。
从 <if> 到 <foreach>,从 <where> 到 <trim>,每个标签都有其最佳使用场景。在实际项目中,灵活组合这些标签,才能写出既优雅又高效的持久层代码。
最后记住一句话:动态 SQL 让你写更少的代码,做更多的事------但前提是你真的理解每个标签的行为边界。