MyBatis 动态 SQL 详解:从原理到实战

一、为什么需要动态 SQL?

在实际开发中,我们几乎不可能写出"一成不变"的 SQL。最典型的场景就是多条件组合查询------用户可能只填了姓名,也可能同时填了姓名、类型和状态,还可能什么都不填。

如果用传统 JDBC 拼接字符串,你会写出这样的代码:

java 复制代码
StringBuilder sql = new StringBuilder("select * from employee where 1=1");
if (name != null) {
    sql.append(" and name like '%").append(name).append("%'");
}
if (status != null) {
    sql.append(" and status = ").append(status);
}
// ... 一堆 if-else,容易拼错、容易 SQL 注入

痛点很明显:

  • 拼接易出错:少一个空格、多一个逗号,SQL 就废了
  • SQL 注入风险:手动拼字符串无法使用预编译参数
  • 可读性差:业务逻辑和 SQL 拼接混在一起,代码像面条

MyBatis 的动态 SQL 机制就是为了优雅地解决这些问题而诞生的。

二、动态 SQL 的本质

MyBatis 动态 SQL 的底层原理是 OGNL 表达式 + XML 标签解析 。MyBatis 在解析 Mapper XML 时,会将这些标签转换为 SqlNode 对象树,运行时根据传入参数的值,动态地裁剪和拼接 SQL 片段,最终生成一条完整的、可执行的预编译 SQL。

简单理解:动态 SQL = 在 XML 里写 if/else,MyBatis 帮你在运行时智能拼 SQL

三、九大核心标签全解析

3.1 <if> ------ 条件判断,最基础的守门员

<if> 是使用频率最高的动态 SQL 标签,用于根据参数值决定是否拼接某段 SQL。

XML 复制代码
<select id="pageQuery" resultType="Employee">
    select * from employee
    <where>
        <if test="name != null and name != ''">
            and name like concat('%', #{name}, '%')
        </if>
        <if test="status != null">
            and status = #{status}
        </if>
    </where>
    order by create_time desc
</select>

要点:

  • test 属性使用 OGNL 表达式,支持 !===andor 等逻辑运算
  • 字符串类型建议同时判断 != null!= '',否则空字符串会拼出无效条件
  • <if> 体内的 SQL 片段可以包含 and/or 前缀,配合 <where> 使用可以自动去除

3.2 <where> ------ 智能处理 WHERE 子句

<where> 标签会做两件聪明的事:

  1. 当内部有条件满足时,自动添加 WHERE 关键字
  2. 自动去除条件开头多余的 ANDOR
XML 复制代码
<!-- 如果 name 和 status 都为 null,生成:select * from employee -->
<!-- 如果只有 name 不为 null,生成:select * from employee WHERE name like ... -->
<!-- 不会出现 "WHERE and name like ..." 的尴尬 -->
<select id="list" resultType="Employee">
    select * from employee
    <where>
        <if test="name != null">
            and name like concat('%', #{name}, '%')
        </if>
        <if test="status != null">
            and status = #{status}
        </if>
    </where>
</select>

注意: <where> 只会去除开头 的多余 AND/OR,不会处理中间和结尾的。所以 <if> 里的条件统一以 and 开头是最佳实践。

3.3 <set> ------ 更新操作的好搭档

更新操作最头疼的问题是:用户只改了名字,你总不能把其他字段都更新为 null 吧?<set> 标签专门解决这个问题。

XML 复制代码
<update id="update" parameterType="Employee">
    update employee
    <set>
        <if test="name != null">name = #{name},</if>
        <if test="username != null">username = #{username},</if>
        <if test="phone != null">phone = #{phone},</if>
        <if test="sex != null">sex = #{sex},</if>
        <if test="status != null">status = #{status},</if>
    </set>
    where id = #{id}
</update>

<set> 标签的作用:

  1. 自动在内容前添加 SET 关键字
  2. 自动去除最后一个多余的逗号 ,

这就是为什么每个 <if> 里末尾都写了逗号,但不用担心 SQL 报错。

3.4 <choose>/<when>/<otherwise> ------ 多选一的条件分支

<if> 是"满足就拼",但有时候你需要的是"只选一个"的逻辑,类似 Java 的 switch-case

XML 复制代码
<select id="listByPriority" resultType="Employee">
    select * from employee
    <where>
        <choose>
            <when test="id != null">
                id = #{id}
            </when>
            <when test="name != null and name != ''">
                name like concat('%', #{name}, '%')
            </when>
            <otherwise>
                status = 1
            </otherwise>
        </choose>
    </where>
</select>

执行逻辑:

  • 按顺序判断 <when>,一旦有满足的就使用,后面的不再判断
  • 如果所有 <when> 都不满足,执行 <otherwise>
  • <otherwise> 可以省略

适用场景: 优先级查询、互斥条件选择。

3.5 <foreach> ------ 批量操作之王

当你需要处理 IN 查询或批量插入时,<foreach> 是不可或缺的。

IN 查询
XML 复制代码
<select id="getByIds" resultType="Category">
    select * from category
    where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

生成效果: select * from category where id in (1, 2, 3)

批量插入
XML 复制代码
<insert id="insertBatch">
    insert into dish_flavor (dish_id, name, value)
    values
    <foreach collection="flavors" item="flavor" separator=",">
        (#{flavor.dishId}, #{flavor.name}, #{flavor.value})
    </foreach>
</insert>

生成效果:

sql 复制代码
insert into dish_flavor (dish_id, name, value) values (1,'辣度','微辣'), (1,'甜度','半糖')

属性说明:

属性 说明
collection 集合参数名,对应接口方法的 @Param 值或参数属性名
item 迭代变量名,在 #{} 中引用
index 索引变量名(List 为下标,Map 为 key)
open 整个循环体前添加的字符串
close 整个循环体后添加的字符串
separator 每次迭代之间的分隔符

⚠️ 注意: 批量插入时,MySQL 对 SQL 长度有限制(max_allowed_packet,默认 4MB),数据量特别大时建议分批插入。

3.6 <trim> ------ 万能裁剪器

<where><set> 本质上都是 <trim> 的语法糖:

XML 复制代码
<!-- where 等价于 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    ...
</trim>

<!-- set 等价于 -->
<trim prefix="SET" suffixOverrides=",">
    ...
</trim>

<trim> 的四个属性:

属性 说明
prefix 给内容整体添加的前缀
suffix 给内容整体添加的后缀
prefixOverrides 去除内容开头指定的字符串
suffixOverrides 去除内容末尾指定的字符串

自定义 trim 示例:

XML 复制代码
<select id="queryByCondition" resultType="Employee">
    select * from employee
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="name != null">
            and name like concat('%', #{name}, '%')
        </if>
        <if test="status != null">
            and status = #{status}
        </if>
    </trim>
</select>

当只需要 <where><set> 的能力时,直接用它们更简洁;遇到更复杂的裁剪需求时,<trim> 才上场。

3.7 <sql> / <include> ------ SQL 片段复用

当你发现多个查询中重复出现相同的字段列表或条件片段时,可以用 <sql> 提取公共部分,用 <include> 引用。

XML 复制代码
<!-- 定义公共字段 -->
<sql id="employeeColumns">
    id, name, username, phone, sex, id_number, status, create_time, update_time
</sql>

<!-- 定义公共查询条件 -->
<sql id="employeeCondition">
    <if test="name != null and name != ''">
        and name like concat('%', #{name}, '%')
    </if>
    <if test="status != null">
        and status = #{status}
    </if>
</sql>

<!-- 引用 -->
<select id="pageQuery" resultType="Employee">
    select <include refid="employeeColumns"/> from employee
    <where>
        <include refid="employeeCondition"/>
    </where>
    order by create_time desc
</select>

<select id="list" resultType="Employee">
    select <include refid="employeeColumns"/> from employee
    <where>
        <include refid="employeeCondition"/>
    </where>
</select>

3.8 <bind> ------ 变量绑定

<bind> 允许你在 SQL 中创建一个变量,对 OGNL 表达式的结果进行预处理。

XML 复制代码
<select id="fuzzyQuery" resultType="Employee">
    <bind name="pattern" value="'%' + name + '%'" />
    select * from employee
    where name like #{pattern}
</select>

典型应用场景:

  • 模糊查询的通配符拼接(避免在每个数据库方言中写不同的 concat)
  • 对参数做预处理(如大小写转换、字符串截取等)
XML 复制代码
<!-- 大小写不敏感查询 -->
<select id="search" resultType="Employee">
    <bind name="lowerName" value="name.toLowerCase()" />
    select * from employee
    where LOWER(name) like CONCAT('%', #{lowerName}, '%')
</select>

四、动态 SQL 的性能考量

4.1 SQL 缓存与动态 SQL

MyBatis 有一个重要的内部机制:每个查询都会生成一个 MappedStatement,其中包含 SQL 的解析模板。动态 SQL 的条件分支不会在启动时就固定,而是在每次执行时根据参数重新解析。

但这并不意味着严重的性能问题,因为 MyBatis 内部对 SQL 解析做了缓存优化------相同参数组合生成的 SQL 会被缓存,不会每次都重新解析 OGNL。

4.2 批量操作的选择

方案 优点 缺点
<foreach> 拼接 一次 SQL 完成 SQL 过长可能超限
Batch Executor 安全,无 SQL 长度限制 多次网络交互
分批 foreach 兼顾效率和安全 需要手动分批

推荐: 数据量小(< 500 条)用 <foreach>;数据量大用 MyBatis 的 BatchExecutor 或分批插入。

五、一张图总结

XML 复制代码
动态 SQL 标签速查
├── 条件判断
│   ├── <if>           → 满足条件就拼接
│   └── <choose>       → 多选一(when/otherwise)
├── 子句修饰
│   ├── <where>        → 自动加 WHERE,去开头 AND/OR
│   ├── <set>          → 自动加 SET,去末尾逗号
│   └── <trim>         → 自定义前后缀裁剪(where/set 的底层实现)
├── 集合迭代
│   └── <foreach>      → IN 查询、批量插入
├── 复用与绑定
│   ├── <sql>/<include>→ SQL 片段定义与引用
│   └── <bind>         → OGNL 变量绑定
└── 注解方式
    └── <script>       → 注解中写动态 SQL

结语

动态 SQL 是 MyBatis 最核心的特性之一,掌握它不仅是"会用几个标签",更关键的是理解它的设计哲学------将 SQL 的静态结构与运行时的动态条件解耦

<if><foreach>,从 <where><trim>,每个标签都有其最佳使用场景。在实际项目中,灵活组合这些标签,才能写出既优雅又高效的持久层代码。

最后记住一句话:动态 SQL 让你写更少的代码,做更多的事------但前提是你真的理解每个标签的行为边界。

相关推荐
浮尘笔记1 小时前
在Snowy后台无需编码实现自动化生成CRUD操作流程
java·开发语言·经验分享·spring boot·后端·程序人生·mybatis
-星空下无敌1 小时前
IDEA 2025.3.1最新最全下载、安装、配置及使用教程(保姆级教程)
java·ide·intellij-idea
JAVA面经实录9171 小时前
Spring Boot + Spring AI 一体化实战全文档
java·人工智能·spring boot·spring
希望永不加班1 小时前
SpringBoot 接口签名验证(AppKey/Secret)
java·spring boot·后端·spring
fengxin_rou2 小时前
RabbitMQ安装教程:windows本地安装和docker部署
java·分布式·后端·rabbitmq
a8a3022 小时前
Laravel7.x核心特性全解析
java·spring boot·后端
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第19题:HashMap的key如何减少发生哈希冲突
java·开发语言·后端·面试·哈希算法·hash-index·hash
coderlin_2 小时前
Langgraph项目三 agent搭建
java·数据库·redis
xyx-3v2 小时前
信号量(二进制/计数)
java·linux·数据库