MyBatis动态SQL详解

文章目录

    • [**1. if:最常用的条件拼接**](#1. if:最常用的条件拼接)
    • [**2. where:自动处理多余的 AND / OR**](#2. where:自动处理多余的 AND / OR)
    • [**3. set:专门给 UPDATE 用,自动去掉最后一个逗号**](#3. set:专门给 UPDATE 用,自动去掉最后一个逗号)
    • [**4. choose / when / otherwise:多分支二选一**](#4. choose / when / otherwise:多分支二选一)
    • [**5. trim:通用修整器,where 和 set 都是它的特化版**](#5. trim:通用修整器,where 和 set 都是它的特化版)
    • [**6. foreach:批量参数、IN 查询、批量插入的核心**](#6. foreach:批量参数、IN 查询、批量插入的核心)
    • [**7. bind:先绑定变量,再复用**](#7. bind:先绑定变量,再复用)
    • [**8. script:注解方式下的动态 SQL**](#8. script:注解方式下的动态 SQL)
    • [**9. 参数访问规则**](#9. 参数访问规则)
    • [**11. #{} 和 {} 的区别\*\*](#{} 和 {} 的区别**)
    • [**12. 常见坑**](#12. 常见坑)

核心概念

MyBatis 的"动态 SQL 标签"本质上就是:根据入参,按条件拼出最终 SQL。

它解决的是这类问题:

  • 查询条件有时传、有时不传
  • WHERE 后面的 AND/OR 容易多出来
  • UPDATE 时只更新非空字段
  • IN 查询参数个数不固定
  • 多分支 SQL 需要二选一、三选一

MyBatis 动态 SQL 主要依赖 XML 标签和 test 表达式。test 用的是 OGNL 表达式。


1. if:最常用的条件拼接

sql 复制代码
<select id="selectUser" resultType="User">
  SELECT * FROM user
  WHERE 1 = 1
  <if test="name != null and name != ''">
    AND name = #{name}
  </if>
  <if test="age != null">
    AND age = #{age}
  </if>
</select>

作用:

  • test 为真,标签内 SQL 生效
  • test 为假,整段不拼接

适合:

  • 单个条件是否参与查询
  • 非空才更新字段
  • 按参数控制排序、过滤、范围

常见坑:

  • 字符串判空不能只写 name != null,很多场景还要加 name != ''
  • 0、false 不是 null,不要误判
  • if 太多时,SQL 可读性会变差

2. where:自动处理多余的 AND / OR

sql 复制代码
<select id="selectUser" resultType="User">
  SELECT * FROM user
  <where>
    <if test="name != null and name != ''">
      AND name = #{name}
    </if>
    <if test="age != null">
      AND age = #{age}
    </if>
  </where>
</select>

它会做两件事:

  • 只要内部有内容,就自动加 WHERE
  • 自动去掉开头多余的 AND 或 OR

比如上面如果只有 age 生效,最终 SQL 会是:

sql 复制代码
SELECT * FROM user WHERE age = ?

而不是:

sql 复制代码
SELECT * FROM user WHERE AND age = ?

这比手写 WHERE 1=1 更干净。


3. set:专门给 UPDATE 用,自动去掉最后一个逗号

sql 复制代码
<update id="updateUser">
  UPDATE user
  <set>
    <if test="name != null">name = #{name},</if>
    <if test="age != null">age = #{age},</if>
    <if test="email != null">email = #{email},</if>
  </set>
  WHERE id = #{id}
</update>

作用:

  • 自动加 SET
  • 自动去掉最后多余的 ,

最终不会出现:

sql 复制代码
UPDATE user SET name = ?, age = ?, WHERE id = ?

这是"按非空字段更新"的标准写法。


4. choose / when / otherwise:多分支二选一

类似 Java 里的 switch / if-else if-else。

sql 复制代码
<select id="queryUser" resultType="User">
  SELECT * FROM user
  <where>
    <choose>
      <when test="id != null">
        id = #{id}
      </when>
      <when test="name != null and name != ''">
        name = #{name}
      </when>
      <otherwise>
        status = 1
      </otherwise>
    </choose>
  </where>
</select>

规则:

  • 只会进入第一个命中的 when
  • 如果都不命中,就走 otherwise

适合:

  • "优先按 ID 查,否则按 name 查,否则查默认数据"
  • 多条件互斥的业务逻辑

5. trim:通用修整器,where 和 set 都是它的特化版

sql 复制代码
<select id="selectUser" resultType="User">
  SELECT * FROM user
  <trim prefix="WHERE" prefixOverrides="AND |OR ">
    <if test="name != null and name != ''">
      AND name = #{name}
    </if>
    <if test="age != null">
      AND age = #{age}
    </if>
  </trim>
</select>

含义:

  • prefix="WHERE":有内容时前面加 WHERE
  • prefixOverrides="AND |OR ":去掉开头的 AND / OR

更新场景也能用:

sql 复制代码
<trim prefix="SET" suffixOverrides=",">
  <if test="name != null">name = #{name},</if>
  <if test="age != null">age = #{age},</if>
</trim>

适合:

  • 你觉得 where / set 不够灵活时
  • 需要自定义前缀、后缀、去除规则

6. foreach:批量参数、IN 查询、批量插入的核心

查询 IN:

sql 复制代码
<select id="selectByIds" resultType="User">
  SELECT * FROM user
  WHERE id IN
  <foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

如果 ids = 1,2,3,最终 SQL 类似:

sql 复制代码
SELECT * FROM user WHERE id IN (?, ?, ?)

批量插入:

sql 复制代码
<insert id="batchInsert">
  INSERT INTO user(name, age)
  VALUES
  <foreach collection="list" item="item" separator=",">
    (#{item.name}, #{item.age})
  </foreach>
</insert>

常用属性:

  • collection:遍历对象名
  • item:当前元素变量名
  • index:下标或 key
  • open:开始符号
  • separator:分隔符
  • close:结束符号

常见坑:

  • collection 名字要和入参对应
    • 单个 List 参数常见是 list
    • 单个数组参数常见是 array
    • 使用 @Param("ids") 后就写 ids
  • 空集合会生成非法 SQL
    例如 IN (),要在业务层或 XML 中提前处理

7. bind:先绑定变量,再复用

sql 复制代码
<select id="selectByNameLike" resultType="User">
  <bind name="keyword" value="'%' + name + '%'" />
  SELECT * FROM user
  WHERE name LIKE #{keyword}
</select>

作用:

  • 先用 OGNL 生成一个新变量
  • 后续 SQL 中可直接用这个变量

典型用途:

  • LIKE 模糊查询
  • 对参数做轻量预处理
  • 避免把拼接逻辑写得很乱

比直接写 ${} 安全得多。


8. script:注解方式下的动态 SQL

如果你不是写 XML,而是写 Mapper 注解,可以用 :

java 复制代码
@Select("""
<script>
SELECT * FROM user
<where>
  <if test="name != null and name != ''">
    AND name = #{name}
  </if>
  <if test="age != null">
    AND age = #{age}
  </if>
</where>
</script>
""")
List<User> selectUser(@Param("name") String name, @Param("age") Integer age);

作用:

  • 让注解中的 SQL 也支持动态标签

但实际项目里,复杂 SQL 还是更推荐 XML,维护性更好。


9. 参数访问规则

这是很多人最容易混乱的地方。

单个普通参数

java 复制代码
User selectById(Long id);

XML 中一般可直接用:

sql 复制代码
#{id}

有时也可通过 _parameter 访问。

多个参数

java 复制代码
List<User> query(@Param("name") String name, @Param("age") Integer age);

XML 中:

sql 复制代码
name != null
#{name}
#{age}

对象参数

java 复制代码
List<User> query(UserQuery query);

XML 中可直接写对象属性:

sql 复制代码
query.name   // 某些场景
name         // 更常见
age

集合参数

java 复制代码
List<User> selectByIds(@Param("ids") List<Long> ids);

XML 中:

sql 复制代码
<foreach collection="ids" item="id">

建议:

  • 多参数时始终用 @Param
  • 集合参数也显式命名,少踩坑

11. #{} 和 ${} 的区别

#{}

  • 预编译占位符
  • 自动加引号、类型处理
  • 安全,推荐默认使用

${}

  • 直接文本替换
  • 不会预编译
  • 有 SQL 注入风险

例子:

sql 复制代码
WHERE name = #{name}

如果 name = "Tom",会变成参数化 SQL。

而:

sql 复制代码
ORDER BY ${column}

会把 column 原样拼到 SQL 里。

结论:

  • 值用 #{}
  • 表名、列名、排序字段这类"SQL 标识符"才可能用 ${}
  • 用 ${} 时必须白名单校验

12. 常见坑

1. where 里不要自己乱写固定 AND

让 where/trim 去处理前缀更稳。

2. foreach 空集合问题

如果集合为空,IN () 会报错。常见做法:

  • 业务层提前返回空结果
  • 或 XML 中先判断 list != null and list.size > 0

3. LIKE 不要直接用 ${}

错误示例:

sql 复制代码
name LIKE '%${name}%'

推荐:

sql 复制代码
<bind name="keyword" value="'%' + name + '%'" />
name LIKE #{keyword}

4. 更新时漏掉 WHERE

动态 set 很方便,但 WHERE id = #{id} 不能漏,不然可能全表更新。

5. test 表达式写错属性名

OGNL 访问的是参数对象属性,不是数据库字段名。