MyBatis 支持动态 SQL 吗?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
标签掌握度:面试官不仅仅是想知道 "支持不支持",更是想考察你是否熟悉 MyBatis 提供的各种动态 SQL 标签(
<if>、<where>、<foreach>、<choose>等),以及各自的适用场景。 -
实战编写能力:看你能否根据业务需求正确组合使用多个动态 SQL 标签,避免写出有 bug 的 SQL(比如多余的逗号、AND 关键字残留等)。
-
原理理解:高级一点的考察,看你知不知道动态 SQL 的底层是如何解析和执行的(OGNL 表达式、
SqlNode树)。
核心答案
MyBatis 完全支持动态 SQL,而且动态 SQL 是 MyBatis 最强大的特性之一。它提供了 9 种动态 SQL 标签,可以根据传入参数动态地拼接 SQL 语句:
| 标签 | 作用 | 使用频率 |
|---|---|---|
<if> | 条件判断,满足则拼接 | ⭐⭐⭐⭐⭐ |
<where> | 智能 WHERE 子句,自动处理前缀 AND/OR | ⭐⭐⭐⭐⭐ |
<foreach> | 遍历集合,常用于 IN 查询和批量操作 | ⭐⭐⭐⭐⭐ |
<set> | 智能 SET 子句,自动处理多余逗号 | ⭐⭐⭐⭐ |
<choose> / <when> / <otherwise> | 多条件分支,类似 switch-case | ⭐⭐⭐ |
<trim> | 自定义前后缀裁剪,最灵活 | ⭐⭐ |
<sql> / <include> | SQL 片段抽取与复用 | ⭐⭐⭐ |
<bind> | 创建变量并绑定到上下文 | ⭐⭐ |
<script> | 在注解 SQL 中使用动态 SQL 标签 | ⭐ |
一句话结论:动态 SQL 是 MyBatis 的核心优势,通过标签组合可以根据参数动态生成不同的 SQL,告别 JDBC 时代手动拼字符串的痛苦。
深度解析
一、动态 SQL 的解析执行流程
MyBatis 动态 SQL 的解析执行分为三个阶段:
-
阶段一(XML 解析):MyBatis 启动时解析 Mapper XML,将动态 SQL 标签解析为一棵
SqlNode树。每个标签对应一个SqlNode实现类(IfSqlNode、WhereSqlNode、ForEachSqlNode等)。这个过程只做一次,解析结果会被缓存。 -
阶段二(运行时求值):每次方法调用时,MyBatis 遍历
SqlNode树,根据传入参数(通过 OGNL 表达式求值)决定哪些节点需要输出,哪些跳过。 -
阶段三(SQL 拼接):所有激活的
SqlNode按顺序拼接成最终的 SQL 字符串,<where>、<set>、<trim>等标签还会智能处理多余的前缀 / 后缀(比如去掉开头的AND、末尾的逗号)。
二、常用标签详解与代码示例
1. <if> —— 条件判断
最常见的动态 SQL 标签,根据参数是否为空决定是否拼接条件:
<select id="selectByCondition" resultType="User">
SELECT * FROM t_user
WHERE status = 1
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null">
AND email = #{email}
</if>
</select>
test属性中使用 OGNL 表达式进行条件判断- 多个
<if>之间是 "与" 的关系,所有满足条件的子句都会被拼接
2. <where> —— 智能 WHERE 子句
上面的写法有个问题:如果所有 <if> 都不满足,SQL 会变成 SELECT * FROM t_user WHERE,语法错误。用 <where> 可以自动处理:
<select id="selectByCondition" resultType="User">
SELECT * FROM t_user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
<where> 的智能之处:
- 当内部所有条件都不满足时,不会生成
WHERE关键字 - 当有条件满足时,自动生成
WHERE,并去掉第一个条件前面多余的AND/OR
3. <foreach> —— 遍历集合
批量操作必备,常用于 IN 查询和批量插入:
<!-- IN 查询 -->
<select id="selectByIds" resultType="User">
SELECT * FROM t_user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO t_user (username, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.email})
</foreach>
</insert>
<foreach> 核心属性:
| 属性 | 说明 |
|---|---|
collection | 集合参数名(对应 @Param 注解的值) |
item | 遍历时的当前元素变量名 |
index | 索引变量名(List 是下标,Map 是 key) |
open | 拼接起始符号 |
close | 拼接结束符号 |
separator | 每次遍历之间的分隔符 |
4. <set> —— 智能 SET 子句
更新操作时,只更新非空字段,并自动去掉多余的逗号:
<update id="updateSelective">
UPDATE t_user
<set>
<if test="username != null">
username = #{username},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="status != null">
status = #{status},
</if>
</set>
WHERE id = #{id}
</update>
<set> 会自动去掉最后一个条件后面多余的逗号。如果没有 <set>,当只有 username 非空时,SQL 会变成 UPDATE t_user SET username = #{username}, WHERE id = #{id},逗号直接出现在 WHERE 前面,语法错误。
5. <choose> / <when> / <otherwise> —— 多条件分支
类似 Java 的 switch-case,只命中第一个满足条件的分支:
<select id="selectByPriority" resultType="User">
SELECT * FROM t_user
<where>
<choose>
<when test="id != null">
id = #{id}
</when>
<when test="username != null">
username = #{username}
</when>
<otherwise>
status = 1
</otherwise>
</choose>
</where>
</select>
- 有
id就按id查,没有id有username就按username查,都没有就按status = 1查 - 与多个
<if>的区别:<choose>只会命中一个分支,<if>可以同时命中多个
6. <trim> —— 自定义裁剪
<where> 和 <set> 的底层其实都是基于 <trim> 实现的:
<!-- <where> 等价于 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
<!-- <set> 等价于 -->
<trim prefix="SET" suffixOverrides=",">
...
</trim>
| 属性 | 说明 |
|---|---|
prefix | 内容不为空时,添加的前缀 |
suffix | 内容不为空时,添加的后缀 |
prefixOverrides | 需要去除的头部内容 |
suffixOverrides | 需要去除的尾部内容 |
7. <sql> / <include> —— SQL 片段复用
将重复的 SQL 片段抽取出来复用:
<!-- 定义 SQL 片段 -->
<sql id="userColumns">
id, username, email, status, created_time
</sql>
<!-- 引用 SQL 片段 -->
<select id="selectById" resultType="User">
SELECT <include refid="userColumns"/>
FROM t_user
WHERE id = #{id}
</select>
<select id="selectAll" resultType="User">
SELECT <include refid="userColumns"/>
FROM t_user
</select>
三、动态 SQL 的底层原理——OGNL 表达式
MyBatis 动态 SQL 的条件判断(test 属性)使用的是 OGNL(Object-Graph Navigation Language) 表达式:
<!-- 常用 OGNL 表达式示例 -->
<if test="name != null"> <!-- 判空 -->
<if test="name != null and name != ''"> <!-- 判空且非空字符串 -->
<if test="age > 18"> <!-- 数值比较 -->
<if test="list != null and list.size() > 0"> <!-- 集合非空 -->
<if test="type == 'admin'"> <!-- 字符串相等 -->
OGNL 表达式的执行是通过 Ognl.getValue() 方法实现的,MyBatis 将参数对象包装为 OGNL 的上下文,然后对 test 属性的表达式进行求值,返回 true 或 false。
四、常见误区
- 误区一:
<where>会自动添加所有条件- 不是,
<where>只负责处理WHERE关键字和前缀AND/OR,条件是否拼接还是由内部的<if>等标签决定。
- 不是,
- 误区二:
<foreach>的collection属性随便写- 当方法有多个参数时,必须使用
@Param注解指定参数名,collection的值要和@Param的值一致。单个参数且类型为List时,默认可用list;类型为数组时,默认可用array。
- 当方法有多个参数时,必须使用
- 误区三:动态 SQL 会影响性能
- 影响很小。
SqlNode树只在启动时解析一次并缓存,运行时只是遍历树做条件求值和字符串拼接,开销远小于一次数据库 IO。
- 影响很小。
面试高频追问
- 追问一:
<where>和直接写WHERE 1=1有什么区别?WHERE 1=1是一种老式写法,通过一个永远为真的条件让后续所有条件都能用AND开头。缺点是多了一个无意义的条件,某些数据库优化器可能无法优化掉。<where>是更优雅的方式,自动处理前缀,不产生冗余条件。
- 追问二:
<foreach>做批量插入时,数据量很大怎么办?- MySQL 对 SQL 长度有限制(
max_allowed_packet),批量插入数据量过大时会报错。解决方案:在 Java 层对集合做分片(比如每 1000 条执行一次),或者在数据库连接 URL 中配置rewriteBatchedStatements=true开启批量重写。
- MySQL 对 SQL 长度有限制(
- 追问三:动态 SQL 的 OGNL 表达式有什么安全风险?
- OGNL 表达式本身不直接面对用户输入(
test属性是开发者在 XML 中写死的),所以不存在注入风险。但要注意test中调用的方法(如.size())可能抛空指针异常,建议先判空。
- OGNL 表达式本身不直接面对用户输入(
常见面试变体
- 变体一:"MyBatis 的动态 SQL 标签有哪些?"
- 变体二:"
<where>和<trim>有什么区别?" - 变体三:"MyBatis 如何实现批量操作?"
记忆口诀
if 判空拼条件,where 自动去前缀;foreach 遍历做批量,set 更新去尾逗;choose 分支只选一,trim 裁剪最灵活;底层 OGNL 来求值,SqlNode 树动态拼。
总结
MyBatis 完全支持动态 SQL,这是它最核心的特性之一。通过 <if>、<where>、<foreach>、<set>、<choose>、<trim> 等标签,可以根据参数动态生成不同的 SQL。底层原理是将 XML 中的动态 SQL 解析为 SqlNode 树,运行时通过 OGNL 表达式求值,动态拼接最终的 SQL 字符串。