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/

面试考察点

  1. 标签掌握度:面试官不仅仅是想知道 "支持不支持",更是想考察你是否熟悉 MyBatis 提供的各种动态 SQL 标签(<if><where><foreach><choose> 等),以及各自的适用场景。

  2. 实战编写能力:看你能否根据业务需求正确组合使用多个动态 SQL 标签,避免写出有 bug 的 SQL(比如多余的逗号、AND 关键字残留等)。

  3. 原理理解:高级一点的考察,看你知不知道动态 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 实现类(IfSqlNodeWhereSqlNodeForEachSqlNode 等)。这个过程只做一次,解析结果会被缓存。

  • 阶段二(运行时求值):每次方法调用时,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 查,没有 idusername 就按 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 属性的表达式进行求值,返回 truefalse

四、常见误区

  1. 误区一:<where> 会自动添加所有条件
    • 不是,<where> 只负责处理 WHERE 关键字和前缀 AND/OR,条件是否拼接还是由内部的 <if> 等标签决定。
  2. 误区二:<foreach>collection 属性随便写
    • 当方法有多个参数时,必须使用 @Param 注解指定参数名,collection 的值要和 @Param 的值一致。单个参数且类型为 List 时,默认可用 list;类型为数组时,默认可用 array
  3. 误区三:动态 SQL 会影响性能
    • 影响很小。SqlNode 树只在启动时解析一次并缓存,运行时只是遍历树做条件求值和字符串拼接,开销远小于一次数据库 IO。

面试高频追问

  1. 追问一<where> 和直接写 WHERE 1=1 有什么区别?
    • WHERE 1=1 是一种老式写法,通过一个永远为真的条件让后续所有条件都能用 AND 开头。缺点是多了一个无意义的条件,某些数据库优化器可能无法优化掉。<where> 是更优雅的方式,自动处理前缀,不产生冗余条件。
  2. 追问二<foreach> 做批量插入时,数据量很大怎么办?
    • MySQL 对 SQL 长度有限制(max_allowed_packet),批量插入数据量过大时会报错。解决方案:在 Java 层对集合做分片(比如每 1000 条执行一次),或者在数据库连接 URL 中配置 rewriteBatchedStatements=true 开启批量重写。
  3. 追问三:动态 SQL 的 OGNL 表达式有什么安全风险?
    • OGNL 表达式本身不直接面对用户输入(test 属性是开发者在 XML 中写死的),所以不存在注入风险。但要注意 test 中调用的方法(如 .size())可能抛空指针异常,建议先判空。

常见面试变体

  • 变体一:"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 字符串。