MyBatis ③动态 SQL 完全指南 —— 从条件拼接到批量操作

文章目录

    • [一、动态 SQL 总述](#一、动态 SQL 总述)
      • [1.1 什么是动态 SQL?为什么需要它?](#1.1 什么是动态 SQL?为什么需要它?)
    • 二、各标签详解
      • [2.1 `<if>` --- 条件判断标签](#2.1 <if> — 条件判断标签)
      • [2.2 `<trim>` --- 万能修剪标签](#2.2 <trim> — 万能修剪标签)
        • [场景一:多字段可选插入(用 `suffixOverrides` 去逗号)](#场景一:多字段可选插入(用 suffixOverrides 去逗号))
        • [场景二:多条件查询(用 `prefixOverrides` 去 AND)](#场景二:多条件查询(用 prefixOverrides 去 AND))
      • [2.3 `<where>` --- 查询专用条件标签](#2.3 <where> — 查询专用条件标签)
      • [2.4 `<set>` --- 更新专用标签](#2.4 <set> — 更新专用标签)
      • [2.5 `<foreach>` --- 遍历集合标签](#2.5 <foreach> — 遍历集合标签)
      • [2.6 `<sql>` + `<include>` --- SQL 片段复用标签](#2.6 <sql> + <include> — SQL 片段复用标签)
    • 三、补充配套知识点
      • [3.1 注解版动态 SQL 的限制与解决方案](#3.1 注解版动态 SQL 的限制与解决方案)
      • [3.2 逻辑删除配套设计](#3.2 逻辑删除配套设计)
      • [3.3 标签选型速查表](#3.3 标签选型速查表)
      • [3.4 易错点总结](#3.4 易错点总结)
    • 四、全文总结
      • [4.1 核心价值回顾](#4.1 核心价值回顾)
      • [4.2 核心知识点复盘](#4.2 核心知识点复盘)

本文基于 Spring Boot + MyBatis 实战项目,系统讲解 MyBatis 动态 SQL 六大核心标签的用法、原理、选型策略和常见踩坑点。


一、动态 SQL 总述

1.1 什么是动态 SQL?为什么需要它?

先看一个真实场景:后台用户列表页,支持按年龄、性别、状态三个条件组合筛选。如果不用动态 SQL,你需要写出以下所有排列组合:

sql 复制代码
-- 无条件:查询全部
SELECT * FROM user_info;
-- 仅按年龄查
SELECT * FROM user_info WHERE age = ?;
-- 按年龄 + 性别查
SELECT * FROM user_info WHERE age = ? AND gender = ?;
-- 按年龄 + 性别 + 状态查
SELECT * FROM user_info WHERE age = ? AND gender = ? AND delete_flag = ?;
-- ... 还有 5 种组合未列出

三个条件 = 8 种组合 = 8 条 SQL。五个条件呢?不用动态 SQL,这种 Mapper 代码根本没法维护

动态 SQL 的核心价值就是:根据传入参数的有无和值的不同,自动拼接生成不同的 SQL 语句 。你只需写一套带判断逻辑的 SQL 模板,MyBatis 在运行期根据实际参数动态组装。MyBatis 为此提供了一整套专用 XML 标签:<if><where><set><trim><foreach><sql> / <include>

注意:动态 SQL 标签默认只在 XML 映射文件中使用 。注解方式需额外包裹 <script> 标签(见第三章)。


二、各标签详解

2.1 <if> --- 条件判断标签

核心作用:单条件判断------当表达式成立时,才将标签内部的 SQL 片段拼接到最终 SQL 中。它是所有动态 SQL 标签中最基础、使用频率最高的一个。

属性说明

属性 说明
test OGNL 表达式,判断条件是否成立(如 age != nullname != null and name != ''

基础示例------多条件可选查询(单用 <if>,有问题版):

xml 复制代码
<!-- ❌ 这个写法有 Bug -->
<select id="searchUser" resultType="com.zmt.mybatisdemo.model.UserInfo">
    SELECT * FROM user_info
    WHERE
        <if test="age != null">age = #{age}</if>
        <if test="gender != null">AND gender = #{gender}</if>
        <if test="deleteFlag != null">AND delete_flag = #{deleteFlag}</if>
</select>

执行结果分析

传参情况 生成的 SQL 结果
三个参数全传 WHERE age = ? AND gender = ? AND delete_flag = ? ✅ 正确
只传 age WHERE age = ? ✅ 正确
只传 gender WHERE AND gender = ? 语法错误WHERE AND
三个都不传 WHERE 语法错误WHERE 后面没条件

问题根因:单独的 <if> 只负责"是否拼入",不负责"拼入后的 SQL 语法是否合法"。多余的 AND 前缀和孤零零的 WHERE 关键字是 <if> 单独使用时最常见的两个坑

要解决这个问题,就需要引入 <where><trim> 标签。


2.2 <trim> --- 万能修剪标签

<trim> 是 MyBatis 动态 SQL 中通用性最强 的标签,<where><set> 本质上都是 <trim> 的特定简化版。它可以在包裹的 SQL 片段前后添加/移除指定字符串。

四大属性

属性 作用 常见取值
prefix 整个语句块开头添加指定前缀 WHERESET(
suffix 整个语句块末尾添加指定后缀 )
prefixOverrides 自动移除语句块开头多余的指定字符串 ANDOR
suffixOverrides 自动移除语句块末尾多余的指定字符串 ,(逗号最常用)
场景一:多字段可选插入(用 suffixOverrides 去逗号)

这是项目中真实存在的代码(UserInfoMapper.xml):

xml 复制代码
<insert id="insertUserByCondition">
    INSERT INTO user_info
    <!-- 字段列表:末尾自动去掉多余的逗号,外层包括号 -->
    <trim suffixOverrides="," suffix=")" prefix="(">
        <if test="username != null">username,</if>
        <if test="password != null">password,</if>
        <if test="age != null">age,</if>
        <if test="gender != null">gender,</if>
        <if test="deleteFlag != null">delete_flag,</if>
    </trim>
    VALUES
    <!-- 值列表:同样逻辑,末尾去逗号 -->
    <trim suffixOverrides="," prefix="(" suffix=")">
        <if test="username != null">#{username},</if>
        <if test="password != null">#{password},</if>
        <if test="age != null">#{age},</if>
        <if test="gender != null">#{gender},</if>
        <if test="deleteFlag != null">#{deleteFlag},</if>
    </trim>
</insert>

执行过程推演 :假设只传了 usernameage 两个字段,genderdeleteFlag 为 null:

  1. 字段 <trim> 内:username, + age, → 末尾逗号被 suffixOverrides="," 自动删除 → 再加上 prefix="("suffix=")" → 最终输出:(username, age)
  2. <trim> 内:#{username}, + #{age}, → 同理 → 最终输出:(#{username}, #{age})
  3. 最终 SQL:INSERT INTO user_info (username, age) VALUES (#{username}, #{age}) ✅ 完美!
场景二:多条件查询(用 prefixOverrides 去 AND)
xml 复制代码
<select id="searchByCondition" resultType="com.zmt.mybatisdemo.model.UserInfo">
    SELECT * FROM user_info
    <!-- 至少一条 if 成立时,开头加 WHERE;同时去除最前面的 AND -->
    <trim prefix="WHERE" prefixOverrides="AND">
        <if test="age != null">age = #{age}</if>
        <if test="gender != null">AND gender = #{gender}</if>
        <if test="deleteFlag != null">AND delete_flag = #{deleteFlag}</if>
    </trim>
</select>

技巧:每个 <if> 内部都以 AND 开头(第一个除外),让 prefixOverrides="AND" 兜底删除。这样所有 <if> 的条件写法保持一致,不用纠结哪个是"第一个条件"。


2.3 <where> --- 查询专用条件标签

<where><trim prefix="WHERE" prefixOverrides="AND | OR"> 的简化版,专为查询设计

核心行为

  1. 内部至少一条 <if> 成立时,自动在 SQL 中插入 WHERE 关键字;
  2. 自动移除第一个成立条件开头 多余的 ANDOR
  3. 如果所有 <if> 都不成立,WHERE 关键字也不会出现。

项目实战代码(UserInfoMapper.xml):

xml 复制代码
<select id="selectUserByCondition" resultType="com.zmt.mybatisdemo.model.UserInfo">
    SELECT * FROM user_info
    <where>
        <if test="age != null">age = #{age}</if>
        <if test="deleteFlag != null">AND delete_flag = #{deleteFlag}</if>
    </where>
</select>

四种传参情况推演

传入参数 <where> 处理后生成的 SQL 片段 最终完整 SQL
age=18, deleteFlag=0 WHERE age = ? AND delete_flag = ? SELECT * FROM user_info WHERE age = ? AND delete_flag = ?
age=18, deleteFlag=null WHERE age = ? SELECT * FROM user_info WHERE age = ?
age=null, deleteFlag=0 WHERE delete_flag = ?(前面的 AND 被自动移除) SELECT * FROM user_info WHERE delete_flag = ?
age=null, deleteFlag=null 空字符串(连 WHERE 也不输出) SELECT * FROM user_info

⚠️ 局限<where> 只能去掉开头AND/OR无法处理末尾多余的逗号 。所以更新场景请用 <set><trim>

反面教材------WHERE 1=1 写法

xml 复制代码
<!-- ❌ 不推荐:用 WHERE 1=1 规避 AND 问题,SQL 丑陋且让索引优化器困惑 -->
SELECT * FROM user_info WHERE 1=1
    <if test="age != null">AND age = #{age}</if>
    <if test="gender != null">AND gender = #{gender}</if>

能用 <where>不要写 WHERE 1=1 。虽然功能上没问题,但它透露着一种"我不知道有 <where> 标签"的信号。


2.4 <set> --- 更新专用标签

<set><trim prefix="SET" suffixOverrides=","> 的简化版,专为 UPDATE 语句设计

核心行为

  1. 自动生成 SET 关键字;
  2. 自动剔除最后一个 <if> 拼接后末尾多余的逗号 ,

项目实战代码(UserInfoMapper.xml):

xml 复制代码
<update id="updateUserByCondition">
    UPDATE user_info
    <set>
        <if test="gender != null">gender = #{gender},</if>
        <if test="deleteFlag != null">delete_flag = #{deleteFlag},</if>
    </set>
    WHERE id = #{id}
</update>

推演 :假设只传了 gender=1deleteFlag=null

  1. <set> 内部:只有 gender = #{gender}, 成立;
  2. suffixOverrides="," 自动去除末尾逗号 → gender = #{gender}
  3. 加上 SET 前缀 → SET gender = #{gender}
  4. 最终 SQL:UPDATE user_info SET gender = ? WHERE id = ?

调用示例(项目测试代码):

java 复制代码
@Test
void updateUserByCondition() {
    UserInfo userInfo = new UserInfo();
    userInfo.setId(6);
    userInfo.setGender(1);
    userInfo.setDeleteFlag(1);
    // 只 set 了 gender 和 deleteFlag,其他字段为 null 不会更新
    Integer rows = userInfoMapperXML.updateUserByCondition(userInfo);
    System.out.println("更新 " + rows + " 条");
}

典型使用场景:前端提交一个"编辑用户"表单,用户只改了性别和状态,后端只更新这两个字段,不会把没传的字段误更新为 null。


2.5 <foreach> --- 遍历集合标签

用于遍历数组、List、Set、Map 等集合,是批量操作的利器。

核心属性

属性 说明 示例值
collection 绑定的集合参数名(List 写 list,数组写 array@Param 写注解别名) idslistarray
item 遍历过程中每个元素的别名 iduser
open 整个遍历结果开头拼接的字符串 (
close 整个遍历结果结尾拼接的字符串 )
separator 每两个遍历项之间的分隔符 ,
index 遍历的索引(可选,Map 时为 key) i

项目实战------批量删除(UserInfoMapper.xml):

xml 复制代码
<delete id="batchDelete">
    DELETE FROM user_info WHERE id IN
    <foreach collection="ids" open="(" close=")" item="id" separator=",">
        #{id}
    </foreach>
</delete>

Mapper 接口

java 复制代码
Integer batchDelete(List<Integer> ids);

调用测试

java 复制代码
@Test
void batchDelete() {
    List<Integer> ids = List.of(14, 15);
    Integer rows = userInfoMapperXML.batchDelete(ids);
    System.out.println("删除 " + rows + " 条");
}

生成 SQL 推演 :传入 ids = [14, 15]<foreach> 遍历 2 次,separator="," 在两次之间插入逗号,open="(" + close=")" 包裹 → (14, 15)。最终 SQL:

sql 复制代码
DELETE FROM user_info WHERE id IN (14, 15)

更多场景示例

xml 复制代码
<!-- 批量新增 -->
<insert id="batchInsert">
    INSERT INTO user_info (username, password, age) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.username}, #{user.password}, #{user.age})
    </foreach>
</insert>

<!-- 多值 OR 查询 -->
<select id="selectByIds" resultType="com.zmt.mybatisdemo.model.UserInfo">
    SELECT * FROM user_info
    WHERE id IN
    <foreach collection="ids" open="(" close=")" item="id" separator=",">
        #{id}
    </foreach>
</select>

易错提醒collection 的值必须和 Mapper 接口入参对应!

  • 方法参数是 List<Integer> ids 且没有 @Paramcollection="list"
  • 方法参数是 Integer[] arr 且没有 @Paramcollection="array"
  • 方法参数是 @Param("ids") List<Integer> idscollection="ids"推荐写法,明确且不依赖类型默认名

2.6 <sql> + <include> --- SQL 片段复用标签

当多个查询语句重复书写相同的字段列表时,<sql> + <include> 提供了"定义一次、全局复用"的能力。

定义公共片段

xml 复制代码
<!-- 定义公共字段列表,id 全局唯一 -->
<sql id="Base_Column_List">
    id, username, password, age, gender, phone,
    delete_flag, create_time, update_time
</sql>

引用公共片段

xml 复制代码
<!-- 方式一:直接引用全部字段 -->
<select id="selectById" resultType="com.zmt.mybatisdemo.model.UserInfo">
    SELECT <include refid="Base_Column_List"/>
    FROM user_info
    WHERE id = #{id}
</select>

<!-- 方式二:引用字段列表,拼接额外字段 -->
<select id="selectWithExtra" resultType="com.zmt.mybatisdemo.model.UserInfo">
    SELECT <include refid="Base_Column_List"/>, extra_field
    FROM user_info
</select>

项目中也有类似的 <sql> 用法(UserInfoMapper.xml):

xml 复制代码
<!-- 将整条基础 SQL 定义为可复用的 sql 片段 -->
<sql id="sql">
    SELECT * FROM `user_info`
</sql>

<!-- 在查询方法中直接引用 -->
<select id="selectAll" resultType="com.zmt.mybatisdemo.model.UserInfo">
    <include refid="sql"/>
</select>

为什么需要 <sql> 复用? 假设数据库 user_info 表新增了一个 email 字段,如果 10 个查询方法都手写了字段列表,你需要改 10 处。用 <sql> 封装后,只要改一处,所有 <include> 引用处自动生效。


三、补充配套知识点

3.1 注解版动态 SQL 的限制与解决方案

前两篇文章提到了注解式和 XML 式两种开发方式。动态 SQL 标签(<if><where> 等)默认只能在 XML 文件中使用 。如果你坚持用注解写动态 SQL,必须将整个动态 SQL 包裹在 <script> 标签内:

java 复制代码
// ✅ 注解版动态 SQL:必须用 <script> 包裹
@Update("<script>" +
        "UPDATE user_info" +
        "  <set>" +
        "    <if test='gender != null'>gender = #{gender},</if>" +
        "    <if test='deleteFlag != null'>delete_flag = #{deleteFlag},</if>" +
        "  </set>" +
        "  WHERE id = #{id}" +
        "</script>")
Integer updateUserByCondition(UserInfo userInfo);

这种写法的问题

  • 字符串拼接维护困难,IDE 不提供 XML 标签的语法提示和校验;
  • SQL 稍复杂时,Java 字符串拼接 + XML 标签混在一起,可读性极差;
  • 无法享受 MyBatis XML 插件的跳转、校验、格式化功能。

结论:动态 SQL 场景是决定"必须用 XML"的最后一根稻草。一旦项目中出现动态 SQL 需求,就不要再犹豫是否用注解了,直接切 XML。

3.2 逻辑删除配套设计

生产环境中,逻辑删除 远比物理删除更常见。设计很简单:

① 数据表加字段

sql 复制代码
ALTER TABLE user_info ADD COLUMN delete_flag TINYINT DEFAULT 0 COMMENT '0=正常, 1=已删除';

② 所有查询默认过滤已删除数据 (配合 <sql> 复用):

xml 复制代码
<!-- 公共的"未删除"过滤条件 -->
<sql id="NotDeleted">
    AND delete_flag = 0
</sql>

<select id="selectAll" resultType="com.zmt.mybatisdemo.model.UserInfo">
    SELECT * FROM user_info
    WHERE 1=1
    <include refid="NotDeleted"/>
</select>

③ "删除"操作改为 UPDATE

java 复制代码
// 逻辑删除:将 delete_flag 从 0 改为 1,而不是 DELETE FROM
@Update("UPDATE user_info SET delete_flag = 1 WHERE id = #{id}")
Integer deleteLogically(@Param("id") Integer id);

逻辑删除 vs 物理删除

维度 逻辑删除 物理删除
实现方式 UPDATE SET delete_flag=1 DELETE FROM
数据留存 ✅ 数据仍在,可查历史、可恢复 ❌ 数据永久消失
误删恢复 ✅ 改回 delete_flag=0 即可 ❌ 只能从备份恢复
查询成本 每次查询要多加 AND delete_flag=0 无额外条件
使用场景 C 端业务数据的默认方案 临时数据、日志清理、极特殊合规要求

3.3 标签选型速查表

需求场景 推荐标签组合 一句话理由
多条件分页 / 模糊查询 <where> + <if> <where> 自动去 AND 前缀,专为查询设计
动态选择性更新字段 <set> + <if> <set> 自动去末尾逗号,专为 UPDATE 设计
多字段可选插入 / 自定义前后缀 <trim> + <if> 万能标签,prefix/suffix/prefixOverrides/suffixOverrides 全支持
批量删除 / 批量新增 / IN 查询 <foreach> 遍历集合生成 (1,2,3) 或多条 VALUES
重复字段列表 / 公共 SQL 片段 <sql> + <include> 定义一次全局复用,字段变更只改一处

3.4 易错点总结

① 只用 <if> 不用 <where> / <trim>------多余 AND 报错

xml 复制代码
<!-- ❌ 当第一个 <if> 不成立、第二个成立时,SQL 会变成 WHERE AND xx -->
SELECT * FROM user_info WHERE
    <if test="age != null">age = #{age}</if>
    <if test="gender != null">AND gender = #{gender}</if>

修复 :用 <where> 包裹,或在前面的 <if> 条件末尾加 AND 配合 <trim prefixOverrides="AND">

② 更新语句只用 <if> 不用 <set>------末尾逗号报错

xml 复制代码
<!-- ❌ 如果 deleteFlag 为 null,SQL 变成 SET gender = ?,(多一个逗号) -->
UPDATE user_info SET
    <if test="gender != null">gender = #{gender},</if>
    <if test="deleteFlag != null">delete_flag = #{deleteFlag},</if>

修复 :用 <set> 包裹,<set> 会自动去掉最后一个逗号。

<foreach>collection 名写错------集合找不到

java 复制代码
// 接口方法
Integer batchDelete(@Param("ids") List<Integer> ids);
xml 复制代码
<!-- ✅ 正确:collection 写 @Param 的别名 "ids" -->
<foreach collection="ids" ...>

<!-- ❌ 错误:写了 "list",但方法上有 @Param("ids") -->
<foreach collection="list" ...>

规则:有 @Paramcollection@Param 的值;无 @Param → List 写 list,数组写 array


四、全文总结

4.1 核心价值回顾

动态 SQL 是 MyBatis 区别于"手写 JDBC"和"简单 ORM"的核心竞争力。它让你用一套 SQL 模板覆盖所有参数组合,避免了手动拼接 SQL 的重复劳动和潜在的注入风险。

六组标签的记忆口诀:

标签 口诀
<if> 有就拼,没有就跳过
<where> 自动加 WHERE,自动去 AND
<set> 自动加 SET,自动去逗号
<trim> 前后缀万能补刀,<where><set> 都是我的简化版
<foreach> 遍历集合,首尾包裹,中间分隔
<sql> + <include> 定义一处,到处复用

4.2 核心知识点复盘

知识模块 要点提炼
<if> test 属性写 OGNL 表达式,单用容易出 AND/逗号/空 WHERE 语法问题
<trim> 四种属性覆盖增删改查全场景;prefixOverrides 去 AND,suffixOverrides 去逗号
<where> 只用于查询;仅内部有成立条件时才输出 WHERE;自动去开头 AND/OR
<set> 只用于更新;自动去末尾逗号
<foreach> collection 名与 @Param 保持同步;open/close/separator 控制输出格式
<sql> + <include> 字段变更只改一处,全项目自动生效
注解动态 SQL <script> 包裹,可读性差,复杂场景不推荐
逻辑删除 delete_flag 字段,删改 UPDATE,查加过滤条件
相关推荐
吃饱了得干活1 小时前
Java并发安全:看这一篇就懂了!
java·后端·面试
前端双越老师1 小时前
前端学习 Java 其实很容易:TS 和 Java 语法的 N 个相同点
java·全栈
evans在进步2 小时前
LeetCode 17:电话号码的字母组合——Java DFS 回溯法详解
java·leetcode·深度优先
2401_850481172 小时前
UVA10391
java
UQR2 小时前
一、Java基础高频面试题
java·开发语言
AC赳赳老秦2 小时前
网页公开附件自动采集:OpenClaw 批量下载页面内嵌 Word/Excel 附件,统一格式后结构化入库
java·python·word·php·excel·deepseek·openclaw
上海云盾商务经理杨杨2 小时前
Tomcat 弱配置漏洞渗透实战!批量 getshell 高频漏洞
java·web安全·tomcat
10mAh3 小时前
【Linux】error while loading shared libraries 怎么解决?——ldd、RPATH 与动态链接排错
java·linux·前端
深念Y3 小时前
stable-diffusion.cpp 的 FLUX.2 Klein 9B 分步
java·前端·数据库