Skip to content

MyBatis动态SQL

动态 SQL 用于根据条件生成不同 SQL。它适合搜索条件不固定、批量插入、按条件更新等场景。

常用标签

标签作用
if条件判断
choose / when / otherwise多分支
where自动处理 where 和 and
set自动处理 update set
foreach遍历集合
trim自定义前后缀处理

查询条件示例

xml
<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标签流程

mermaid
flowchart TD
    A[进入where标签] --> B[判断每个if条件]
    B --> C{是否有条件成立?}
    C -->|否| D[不生成where]
    C -->|是| E[生成where]
    E --> F[去掉开头多余and/or]

foreach批量查询

xml
<select id="selectByIds" resultType="User">
    select * from user
    where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

update set

xml
<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 中如果没有正确限制条件,可能生成全表更新或删除。

危险示例:

xml
delete from user
<where>
    <if test="status != null">
        status = #{status}
    </if>
</where>

当 status 为空时,可能删除全表。删除和更新必须做好参数校验。

order by注入

排序字段不能用 #{},很多人会使用 ${},这时必须做白名单校验。

开发建议

  1. 查询条件多时使用 where
  2. 更新字段不固定时使用 set
  3. 批量 in 使用 foreach
  4. 删除和更新前必须校验关键条件。
  5. ${} 只能配合白名单使用。

动态 SQL 的本质

动态 SQL 不是数据库在执行 XML 标签,而是 MyBatis 在发送 SQL 前,根据 Java 参数把 XML 标签计算成一条最终 SQL。

mermaid
flowchart TD
    A["Mapper 方法参数"] --> B["XML 动态标签"]
    B --> C["OGNL 表达式判断"]
    C --> D["拼接 SQL 片段"]
    D --> E["生成 BoundSql"]
    E --> F["SQL 文本 + 参数映射"]
    F --> G["PreparedStatement 执行"]

例如:

xml
<if test="status != null">
    and status = #{status}
</if>

如果 status = 1,这段 SQL 会被保留;如果 status = null,这段 SQL 不会出现在最终 SQL 中。

排查动态 SQL 问题时,不要只看 XML,要看最终 SQL 和参数。很多线上问题的根因是:条件没有进入、参数名写错、集合为空、whereset 生成了不符合预期的 SQL。

OGNL 表达式怎么理解

MyBatis 动态 SQL 的 test 使用 OGNL 表达式读取参数对象。

java
public class AssetQuery {
    private String assetCode;
    private Integer status;
}
xml
<if test="assetCode != null and assetCode != ''">
    and asset_code = #{assetCode}
</if>

当 Mapper 方法参数是对象时,assetCode 会从对象属性读取。当参数是多个简单值时,建议使用 @Param

java
List<AssetDO> selectPage(@Param("deptId") Long deptId,
                         @Param("status") Integer status);

不加 @Param 的多参数方法可能只能用 param1param2arg0arg1 访问,XML 可读性差,也容易出错。

wheresettrim 的原理

where

where 标签做两件事:

  1. 如果内部没有任何条件成立,不生成 where
  2. 如果内部 SQL 以 andor 开头,会自动去掉开头多余连接词。
xml
<where>
    <if test="assetCode != null">
        and asset_code = #{assetCode}
    </if>
    <if test="status != null">
        and status = #{status}
    </if>
</where>

可能生成:

sql
where asset_code = ?
  and status = ?

也可能什么都不生成。

set

set 标签做两件事:

  1. 自动生成 set
  2. 去掉最后多余的逗号。
xml
<set>
    <if test="assetName != null">asset_name = #{assetName},</if>
    <if test="status != null">status = #{status},</if>
</set>

可能生成:

sql
set asset_name = ?, status = ?

如果所有字段都为空,会生成非法 SQL 或无意义更新,所以 Service 层要校验:至少有一个字段允许更新。

trim

trim 是更通用的前后缀处理。

xml
<trim prefix="where" prefixOverrides="and|or">
    <if test="deptId != null">
        and dept_id = #{deptId}
    </if>
</trim>

whereset 本质上可以看成常用 trim 的封装。

商业 Demo:资产列表动态查询

Mapper:

java
List<AssetDO> selectAssetPage(AssetQuery query);

查询对象:

java
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:

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 &gt;= #{startTime}
        </if>
        <if test="endTime != null">
            and create_time &lt; #{endTime}
        </if>
    </where>
    order by create_time desc
    limit #{offset}, #{pageSize}
</select>

配套索引要根据查询模式设计:

sql
create index idx_asset_dept_status_time
on asset(dept_id, status, create_time);

动态 SQL 只是帮你按条件生成 SQL,不会自动让 SQL 变快。最终仍要看数据库执行计划。

商业 Demo:动态更新防止误覆盖

xml
<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 时,说明数据不存在或已经被别人更新过。

如果没有 idversion

  1. 可能全表更新。
  2. 可能覆盖别人刚改过的数据。
  3. 可能把字段改成 null。
  4. 线上排查很难还原。

foreach 的边界

批量 IN

xml
<foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
</foreach>

风险:

  1. ids 为空时可能生成非法 SQL:where id in
  2. 集合太大时 SQL 很长,数据库解析和优化成本高。
  3. 参数太多可能超过数据库或驱动限制。
  4. IN 查询可能导致执行计划不稳定。

建议:

  1. Service 层先判断空集合,直接返回空结果。
  2. 控制批大小,例如 500 或 1000。
  3. 超大批量改用临时表、中间表或批任务。

${} 白名单排序 Demo

排序字段不能用 #{},因为 ? 只能代表值,不能代表 SQL 结构。

错误:

xml
order by #{sortField}

会变成:

sql
order by ?

这不是按字段排序。

如果必须动态排序:

xml
order by ${sortField} ${sortDirection}

必须在 Java 层白名单:

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";
}

不要把前端传入的字段名直接放进 ${}

线上排查

mermaid
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 必须校验关键条件

面试标准回答

text
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 注入风险。