MyBatis 动态 SQL 完全指南

MyBatis 动态 SQL 完全指南

一、什么是动态 SQL?

动态 SQL 是 MyBatis 最强大的特性之一 。它允许我们在 XML 映射文件中,根据传入参数的不同,动态地拼接 SQL 语句 ,从而避免在 Java 代码中手动拼接字符串(如 WHERE 1=1 AND name = ?)带来的繁琐、易错和 SQL 注入风险。

简单来说:动态 SQL 让我们在 XML 中写"逻辑代码"(如 if、choose、foreach),让 SQL 能够"智能"地适应不同的查询条件。


二、为什么需要动态 SQL?

在实际业务中,查询条件往往是动态的:

  • 用户可能只填了"品牌",没填"价格区间"。
  • 用户可能选了排序字段,也可能没选。
  • 用户可能传了多个 ID 进行批量查询。

如果不用动态 SQL,我们只能写多个不同的 SQL 方法,或者在 Java 中用 StringBuilder 拼接 SQL,不仅代码臃肿,还容易出错。

MyBatis 的动态 SQL 机制,将 SQL 的构建逻辑移到了 XML 中,利用 OGNL(Object-Graph Navigation Language)表达式 来评估条件,实现了声明式的 SQL 构造。


三、核心标签详解

标签 作用 类比 Java
<if> 条件判断,标签体成立则拼接 SQL if
<choose>/<when>/<otherwise> 多分支选择,只选一个 if-else if-else / switch
<where> 智能处理 WHERE 关键字和多余的 AND/OR 无(MyBatis 特有)
<set> 智能处理 SET 关键字和多余的逗号 无(MyBatis 特有)
<trim> 灵活的字符串裁剪/拼接(<where><set>的底层) 无(MyBatis 特有)
<foreach> 遍历集合,常用于 IN 查询或批量插入 for 循环
<bind> 从 OGNL 表达式创建变量,供 SQL 使用 局部变量

四、条件判断标签

1. <if> 标签

最基本的条件标签,test 属性返回 true 时,拼接标签体内的 SQL

基本语法
xml 复制代码
<if test="条件表达式">
    SQL 片段
</if>
示例:多条件查询
xml 复制代码
<select id="selectByCondition" resultType="car">
    SELECT * FROM t_car
    <where>
        <if test="brand != null and brand != ''">
            AND brand LIKE CONCAT('%', #{brand}, '%')
        </if>
        <if test="carType != null and carType != ''">
            AND car_type = #{carType}
        </if>
        <if test="minPrice != null">
            AND guide_price >= #{minPrice}
        </if>
        <if test="maxPrice != null">
            AND guide_price &lt;= #{maxPrice}
        </if>
    </where>
</select>
易错点:空字符串校验
xml 复制代码
<!-- ❌ 错误:只判断了 null,没判断空字符串 -->
<if test="brand != null">
    AND brand = #{brand}
</if>

<!-- ✅ 正确:同时判断 null 和空字符串 -->
<if test="brand != null and brand != ''">
    AND brand = #{brand}
</if>

2. <choose>/<when>/<otherwise> 标签

类似 Java 的 switch 语句,只选择第一个满足条件的分支

xml 复制代码
<select id="selectByKeyword" resultType="car">
    SELECT * FROM t_car
    <where>
        <choose>
            <when test="type == 'brand'">
                AND brand LIKE CONCAT('%', #{keyword}, '%')
            </when>
            <when test="type == 'carNum'">
                AND car_num = #{keyword}
            </when>
            <otherwise>
                AND brand LIKE CONCAT('%', #{keyword}, '%')
            </otherwise>
        </choose>
    </where>
</select>

对比 <if>

  • <if>:多个条件可以同时成立,同时拼接。
  • <choose>:多个条件中最多只有一个生效。

五、智能构建标签

3. <where> 标签

自动处理 WHERE 关键字和多余的 AND/OR,避免 SQL 语法错误。

场景:多个条件动态拼接
xml 复制代码
<select id="selectByCondition" resultType="car">
    SELECT * FROM t_car
    <where>
        <if test="brand != null and brand != ''">
            AND brand = #{brand}
        </if>
        <if test="carType != null and carType != ''">
            AND car_type = #{carType}
        </if>
    </where>
</select>

智能行为

  • 如果没有条件成立,不会生成 WHERE
  • 如果第一个条件是 AND brand = ...<where> 会自动去掉开头的 AND,生成 WHERE brand = ...
等价的手动写法(不推荐)
xml 复制代码
SELECT * FROM t_car
WHERE 1=1
<if test="brand != null">
    AND brand = #{brand}
</if>

4. <set> 标签

专门用于 UPDATE 语句,自动处理 SET 关键字和多余的逗号。

xml 复制代码
<update id="updateSelective">
    UPDATE t_car
    <set>
        <if test="brand != null and brand != ''">
            brand = #{brand},
        </if>
        <if test="carType != null and carType != ''">
            car_type = #{carType},
        </if>
        <if test="guidePrice != null">
            guide_price = #{guidePrice},
        </if>
    </set>
    WHERE id = #{id}
</update>

智能行为 :如果只有一个条件成立,<set> 会自动去掉结尾多余的逗号。

5. <trim> 标签 ------ 底层万能标签

<where><set> 的本质都是 <trim> 的简写。

语法

xml 复制代码
<trim prefix="前缀" suffix="后缀" prefixOverrides="去掉前缀" suffixOverrides="去掉后缀">
    SQL 内容
</trim>
<trim> 实现 <where>
xml 复制代码
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    <if test="brand != null">
        AND brand = #{brand}
    </if>
</trim>

如果条件成立,生成的 SQL 为 WHERE brand = #{brand}AND 被自动去掉)。

<trim> 实现 <set>
xml 复制代码
<trim prefix="SET" suffixOverrides=",">
    <if test="brand != null">
        brand = #{brand},
    </if>
</trim>

如果条件成立,生成的 SQL 为 SET brand = #{brand}(末尾逗号被自动去掉)。


六、迭代标签

6. <foreach> 标签

遍历集合或数组,常用于:

  1. IN 查询WHERE id IN (1, 2, 3)
  2. 批量插入INSERT INTO ... VALUES (..), (..)
语法
xml 复制代码
<foreach collection="集合名" item="元素名" index="索引名"
         open="开头字符" close="结尾字符" separator="分隔符">
    #{元素名}
</foreach>
属性 说明 是否必须
collection 集合/数组的名称
item 当前遍历的元素在循环中的别名
index 当前遍历的索引(List 为下标,Map 为 Key)
open 循环开始前拼接的字符串(如 (
close 循环结束后拼接的字符串(如 )
separator 元素之间的分隔符(如 ,
场景一:IN 查询
java 复制代码
// Mapper 接口
List<Car> selectByIds(@Param("ids") List<Long> ids);
xml 复制代码
<select id="selectByIds" resultType="car">
    SELECT * FROM t_car
    WHERE id IN
    <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>

传入 [1, 2, 3],生成的 SQL:

sql 复制代码
SELECT * FROM t_car WHERE id IN (1, 2, 3)
场景二:批量插入
xml 复制代码
<insert id="insertBatch">
    INSERT INTO t_car (car_num, brand, guide_price) VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.carNum}, #{item.brand}, #{item.guidePrice})
    </foreach>
</insert>
collection 的取值规则
参数类型 collection 写法
直接传 List(无 @Param list
直接传 Array(无 @Param array
Set(无 @Param collection
使用 @Param("ids") ids(注解值)

⚠️ 强烈建议:无论何种集合,都使用 @Param 注解明确命名!


七、变量定义标签

7. <bind> 标签

从 OGNL 表达式创建变量,供 SQL 后续使用。

场景:模糊查询拼接 %
xml 复制代码
<select id="selectByBrand" resultType="car">
    <bind name="pattern" value="'%' + brand + '%'"/>
    SELECT * FROM t_car WHERE brand LIKE #{pattern}
</select>

调用时传入 brand = "宝马"pattern 被绑定为 "%宝马%"

优点 :避免在不同数据库中写不同的 CONCAT 函数,提高了 SQL 的可移植性。

场景:复杂日期计算
xml 复制代码
<bind name="startDate" value="T(java.time.LocalDate).now().minusDays(7)"/>
SELECT * FROM orders WHERE create_time > #{startDate}

八、底层原理:OGNL 表达式

动态 SQL 的条件判断依赖于 OGNL(Object-Graph Navigation Language,对象图导航语言)

常用表达式示例

表达式 说明
brand != null 判断是否为空
brand != null and brand != '' 判断非空且非空字符串
age > 18 数字比较
name == 'admin' 字符串比较
list.size() > 0 集合大小判断
@com.xie.util.Validator@isValid(brand) 调用静态方法

安全警告

OGNL 表达式在 XML 中直接书写,注意:

  • and 必须小写 (不能写成 &&)。
  • 字符串比较用 == (如 'admin')。
  • 注意空指针user.name 如果 usernull,会报错。

九、综合实战:复杂查询示例

xml 复制代码
<select id="selectByComplexCondition" resultType="car">
    SELECT * FROM t_car
    <where>
        <!-- 多字段模糊查询 -->
        <if test="keyword != null and keyword != ''">
            AND (brand LIKE CONCAT('%', #{keyword}, '%')
                 OR car_type LIKE CONCAT('%', #{keyword}, '%'))
        </if>
        
        <!-- 分支选择 -->
        <choose>
            <when test="priceType == 'low'">
                AND guide_price &lt; 10
            </when>
            <when test="priceType == 'mid'">
                AND guide_price BETWEEN 10 AND 30
            </when>
            <when test="priceType == 'high'">
                AND guide_price &gt; 30
            </when>
        </choose>
        
        <!-- 生产日期范围 -->
        <if test="startDate != null and endDate != null">
            AND produce_time BETWEEN #{startDate} AND #{endDate}
        </if>
        
        <!-- 多选过滤 -->
        <if test="types != null and types.size() > 0">
            AND car_type IN
            <foreach collection="types" item="type" open="(" close=")" separator=",">
                #{type}
            </foreach>
        </if>
    </where>
    
    <!-- 动态排序 -->
    <if test="orderColumn != null and orderColumn != ''">
        ORDER BY ${orderColumn}
        <if test="orderDirection != null and orderDirection != ''">
            ${orderDirection}
        </if>
    </if>
</select>

十、常见易错点与避坑指南

1. ANDOR 多余问题

xml 复制代码
<!-- ❌ 错误:不使用 <where>,第一个条件前有 AND -->
SELECT * FROM t_car
WHERE
<if test="brand != null">
    AND brand = #{brand}   <!-- 如果这个条件成立,SQL 变成 WHERE AND brand... -->
</if>

<!-- ✅ 正确:使用 <where> -->
SELECT * FROM t_car
<where>
    <if test="brand != null">
        AND brand = #{brand}
    </if>
</where>

2. 空字符串判断

xml 复制代码
<!-- ❌ 错误:只判 null,不判空字符串 -->
<if test="brand != null">

<!-- ✅ 正确:同时判 null 和空字符串 -->
<if test="brand != null and brand != ''">

3. ${} 注入风险

ORDER BY 等必须使用 ${} 的场景,务必对参数进行白名单校验

java 复制代码
public String validateOrderColumn(String column) {
    Set<String> validColumns = Set.of("id", "brand", "guide_price");
    if (!validColumns.contains(column)) {
        throw new IllegalArgumentException("非法排序字段");
    }
    return column;
}

4. collection 写错

java 复制代码
// Mapper 接口
List<Car> selectByIds(List<Long> ids);  // 无 @Param
xml 复制代码
<!-- ❌ 错误:collection="ids" 但没写 @Param,实际可用的是 list -->
<foreach collection="ids" ...>

<!-- ✅ 正确:使用默认的 list -->
<foreach collection="list" ...>

最佳实践 :一律使用 @Param 注解,一劳永逸。


十一、动态 SQL 最佳实践总结

  1. 尽量使用 <where><set> ,避免手动处理 AND/OR 和逗号。
  2. 条件判断必须同时检查 null 和空字符串!= null and != '')。
  3. 集合遍历务必使用 @Param 注解 明确 collection 名称。
  4. 动态排序/表名使用 ${} 时,必须在代码层做白名单校验,防止 SQL 注入。
  5. 充分利用 <bind> 标签提高 SQL 可移植性(如模糊查询)。
  6. 不要过度使用动态 SQL,过于复杂的动态逻辑建议拆分到 Java 代码中处理。
  7. 开启 MyBatis 日志STDOUT_LOGGING),便于观察最终生成的 SQL 是否正确。

动态 SQL 是 MyBatis 的灵魂,熟练掌握它,可以让你的数据访问层代码既灵活又安全,从容应对各种复杂的业务查询需求。

相关推荐
Csvn12 小时前
📊 SQL 入门 Day 18:索引原理
后端·sql
DBA小马哥12 小时前
关系型数据库核心概念手册:SQL、事务与存储引擎的技术脉络
数据库·sql
灯澜忆梦12 小时前
【MySQL12】进阶篇 | SQL优化
数据库·sql·mysql·性能优化
步行cgn18 小时前
错误:Invalid bound statement (not found) 详解与解决方案
mybatis
盗理者19 小时前
AI Agent 技能分享|SQL 性能诊断与优化
java·sql·spring·skill
上海云盾商务经理杨杨21 小时前
SQL 盲注入渗透实战!无报错页面也能成功注入
数据库·sql
han_hanker1 天前
SQL语法 一个统计的查询
sql
竹枝溪1 天前
MySQL数据库零基础入门
数据库·sql·mysql·navicat·ddl·dml·dql
SemiTris1 天前
从Controller到Tomcat底层请求链路全解析
java·tomcat