MyBatis动态SQL
动态 SQL 用于根据条件生成不同 SQL。它适合搜索条件不固定、批量插入、按条件更新等场景。
常用标签
| 标签 | 作用 |
|---|---|
| if | 条件判断 |
| choose / when / otherwise | 多分支 |
| where | 自动处理 where 和 and |
| set | 自动处理 update set |
| foreach | 遍历集合 |
| trim | 自定义前后缀处理 |
查询条件示例
<select id="listUsers" resultType="User">
select * from user
<where>
<if test="name != null and name != ''">
and name like concat('%', #{name}, '%')
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>where标签流程
flowchart TD
A[进入where标签] --> B[判断每个if条件]
B --> C{是否有条件成立?}
C -->|否| D[不生成where]
C -->|是| E[生成where]
E --> F[去掉开头多余and/or]foreach批量查询
<select id="selectByIds" resultType="User">
select * from user
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>update set
<update id="updateUser">
update user
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</set>
where id = #{id}
</update>set 标签会自动去掉最后多余的逗号。
常见风险
条件为空导致全表操作
动态 SQL 中如果没有正确限制条件,可能生成全表更新或删除。
危险示例:
delete from user
<where>
<if test="status != null">
status = #{status}
</if>
</where>当 status 为空时,可能删除全表。删除和更新必须做好参数校验。
order by注入
排序字段不能用 #{},很多人会使用 ${},这时必须做白名单校验。
开发建议
- 查询条件多时使用
where。 - 更新字段不固定时使用
set。 - 批量 in 使用
foreach。 - 删除和更新前必须校验关键条件。
${}只能配合白名单使用。
动态 SQL 的本质
动态 SQL 不是数据库在执行 XML 标签,而是 MyBatis 在发送 SQL 前,根据 Java 参数把 XML 标签计算成一条最终 SQL。
flowchart TD
A["Mapper 方法参数"] --> B["XML 动态标签"]
B --> C["OGNL 表达式判断"]
C --> D["拼接 SQL 片段"]
D --> E["生成 BoundSql"]
E --> F["SQL 文本 + 参数映射"]
F --> G["PreparedStatement 执行"]例如:
<if test="status != null">
and status = #{status}
</if>如果 status = 1,这段 SQL 会被保留;如果 status = null,这段 SQL 不会出现在最终 SQL 中。
排查动态 SQL 问题时,不要只看 XML,要看最终 SQL 和参数。很多线上问题的根因是:条件没有进入、参数名写错、集合为空、where 或 set 生成了不符合预期的 SQL。
OGNL 表达式怎么理解
MyBatis 动态 SQL 的 test 使用 OGNL 表达式读取参数对象。
public class AssetQuery {
private String assetCode;
private Integer status;
}<if test="assetCode != null and assetCode != ''">
and asset_code = #{assetCode}
</if>当 Mapper 方法参数是对象时,assetCode 会从对象属性读取。当参数是多个简单值时,建议使用 @Param。
List<AssetDO> selectPage(@Param("deptId") Long deptId,
@Param("status") Integer status);不加 @Param 的多参数方法可能只能用 param1、param2 或 arg0、arg1 访问,XML 可读性差,也容易出错。
where、set、trim 的原理
where
where 标签做两件事:
- 如果内部没有任何条件成立,不生成
where。 - 如果内部 SQL 以
and或or开头,会自动去掉开头多余连接词。
<where>
<if test="assetCode != null">
and asset_code = #{assetCode}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>可能生成:
where asset_code = ?
and status = ?也可能什么都不生成。
set
set 标签做两件事:
- 自动生成
set。 - 去掉最后多余的逗号。
<set>
<if test="assetName != null">asset_name = #{assetName},</if>
<if test="status != null">status = #{status},</if>
</set>可能生成:
set asset_name = ?, status = ?如果所有字段都为空,会生成非法 SQL 或无意义更新,所以 Service 层要校验:至少有一个字段允许更新。
trim
trim 是更通用的前后缀处理。
<trim prefix="where" prefixOverrides="and|or">
<if test="deptId != null">
and dept_id = #{deptId}
</if>
</trim>where 和 set 本质上可以看成常用 trim 的封装。
商业 Demo:资产列表动态查询
Mapper:
List<AssetDO> selectAssetPage(AssetQuery query);查询对象:
public class AssetQuery {
private String assetCode;
private Long deptId;
private Integer status;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer offset;
private Integer pageSize;
}XML:
<select id="selectAssetPage" resultMap="AssetMap">
select id, asset_code, asset_name, dept_id, status, create_time
from asset
<where>
<if test="assetCode != null and assetCode != ''">
and asset_code = #{assetCode}
</if>
<if test="deptId != null">
and dept_id = #{deptId}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="startTime != null">
and create_time >= #{startTime}
</if>
<if test="endTime != null">
and create_time < #{endTime}
</if>
</where>
order by create_time desc
limit #{offset}, #{pageSize}
</select>配套索引要根据查询模式设计:
create index idx_asset_dept_status_time
on asset(dept_id, status, create_time);动态 SQL 只是帮你按条件生成 SQL,不会自动让 SQL 变快。最终仍要看数据库执行计划。
商业 Demo:动态更新防止误覆盖
<update id="updateAssetSelective">
update asset
<set>
<if test="assetName != null and assetName != ''">
asset_name = #{assetName},
</if>
<if test="status != null">
status = #{status},
</if>
update_time = now()
</set>
where id = #{id}
and version = #{version}
</update>这里 version 是乐观锁条件。影响行数为 0 时,说明数据不存在或已经被别人更新过。
如果没有 id 和 version:
- 可能全表更新。
- 可能覆盖别人刚改过的数据。
- 可能把字段改成 null。
- 线上排查很难还原。
foreach 的边界
批量 IN:
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>风险:
ids为空时可能生成非法 SQL:where id in。- 集合太大时 SQL 很长,数据库解析和优化成本高。
- 参数太多可能超过数据库或驱动限制。
- 大
IN查询可能导致执行计划不稳定。
建议:
- Service 层先判断空集合,直接返回空结果。
- 控制批大小,例如 500 或 1000。
- 超大批量改用临时表、中间表或批任务。
${} 白名单排序 Demo
排序字段不能用 #{},因为 ? 只能代表值,不能代表 SQL 结构。
错误:
order by #{sortField}会变成:
order by ?这不是按字段排序。
如果必须动态排序:
order by ${sortField} ${sortDirection}必须在 Java 层白名单:
private static final Map<String, String> SORT_FIELD_MAP = new HashMap<>();
static {
SORT_FIELD_MAP.put("createTime", "create_time");
SORT_FIELD_MAP.put("assetCode", "asset_code");
}
public String safeSortField(String input) {
return SORT_FIELD_MAP.getOrDefault(input, "create_time");
}
public String safeDirection(String input) {
return "asc".equalsIgnoreCase(input) ? "asc" : "desc";
}不要把前端传入的字段名直接放进 ${}。
线上排查
flowchart TD
A["动态 SQL 报错或结果异常"] --> B["打开 MyBatis SQL 日志"]
B --> C["确认最终 SQL"]
C --> D["确认参数值和顺序"]
D --> E{"SQL 语法错误?"}
E -- "是" --> F["检查 where/set/foreach/trim"]
E -- "否" --> G{"结果不对?"}
G -- "是" --> H["检查 if 条件和参数名"]
G -- "否" --> I{"执行慢?"}
I -- "是" --> J["拿最终 SQL 去数据库 EXPLAIN"]常见错误:
| 问题 | 表现 | 处理 |
|---|---|---|
| 参数名写错 | 条件没进入或报 getter 找不到 | 多参数使用 @Param |
| 集合为空 | SQL 语法错误 | Service 层判断空集合 |
${} 注入 | SQL 被用户输入改变 | 白名单 |
| 动态 update 全为空 | SQL 非法或只更新 update_time | 校验至少一个字段 |
| 条件为空全表删除 | 生产事故 | delete/update 必须校验关键条件 |
面试标准回答
MyBatis 动态 SQL 是 MyBatis 在执行前根据 Java 参数和 XML 标签生成最终 SQL,不是数据库执行 XML 标签。常用标签有 if、where、set、foreach、choose、trim。where 会自动处理 where 和开头 and/or,set 会自动处理 set 和结尾逗号,foreach 常用于 in 和批量插入。动态 SQL 最终会生成 BoundSql,里面包含 SQL 文本和参数映射。生产中要重点防止条件为空导致全表更新/删除、foreach 集合过大、参数名写错,以及 `${}` 拼接带来的 SQL 注入风险。