Skip to content

动态 SQL

  动态 SQL 是 MyBatis 最强大的特性之一,让你在 XML 中根据条件动态拼接 SQL,彻底告别 Java 代码里的字符串拼接地狱。

一、为什么需要动态 SQL?

java
// 没有动态 SQL,只能这样拼接(噩梦)
String sql = "SELECT * FROM user WHERE 1=1";
if (name != null) {
    sql += " AND name LIKE '%" + name + "%'";  // SQL 注入风险!
}
if (age != null) {
    sql += " AND age = " + age;
}
if (email != null) {
    sql += " AND email = '" + email + "'";
}
xml
<!-- 有了 MyBatis 动态 SQL -->
<select id="selectByCondition" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
        <if test="email != null and email != ''">
            AND email = #{email}
        </if>
    </where>
</select>

二、核心标签

2.1 if(条件判断)

用法:根据条件决定是否拼接 SQL 片段

test 属性中可用的表达式:
  - 逻辑运算:and, or, not
  - 比较运算:==, !=, >, >=, <, <=
  - 属性判断:name != null, name != ''
  - 集合判断:list != null and list.size() > 0
xml
<select id="selectByCondition" resultType="User">
    SELECT * FROM user WHERE 1=1
    <if test="name != null and name != ''">
        AND name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="minAge != null">
        AND age &gt;= #{minAge}
    </if>
    <if test="maxAge != null">
        AND age &lt;= #{maxAge}
    </if>
</select>

2.2 choose-when-otherwise(分支选择)

用法:类似 Java 的 switch-case,只选择第一个匹配的条件

结构:
  <choose>
    <when test="条件1">  SQL片段1  </when>
    <when test="条件2">  SQL片段2  </when>
    <otherwise>        默认SQL片段  </otherwise>
  </choose>
xml
<select id="selectByPriority" resultType="User">
    SELECT * FROM user
    <where>
        <choose>
            <when test="name != null and name != ''">
                AND name = #{name}
            </when>
            <when test="email != null and email != ''">
                AND email = #{email}
            </when>
            <when test="id != null">
                AND id = #{id}
            </when>
            <otherwise>
                AND status = 1
            </otherwise>
        </choose>
    </where>
</select>

2.3 trim-where-set(智能去除)

where 标签

作用:替代 WHERE 1=1 写法,智能处理 AND/OR 前缀

规则:
  ① 如果内部有文本返回,自动加 WHERE 关键字
  ② 如果第一个条件是 AND 或 OR,自动去除
  ③ 如果内部没有文本返回,不输出 WHERE
xml
<!-- 使用 where 标签 -->
<select id="selectByCondition" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null">AND name = #{name}</if>
        <if test="age != null">AND age = #{age}</if>
    </where>
</select>

<!-- 等价于用 trim 实现 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    <if test="name != null">AND name = #{name}</if>
    <if test="age != null">AND age = #{age}</if>
</trim>

set 标签

作用:动态更新时智能处理逗号后缀

规则:
  ① 自动加 SET 关键字
  ② 自动去除末尾多余的逗号
xml
<update id="updateSelective">
    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>

<!-- 等价于用 trim 实现 -->
<trim prefix="SET" suffixOverrides=",">
    <if test="name != null">name = #{name},</if>
    <if test="age != null">age = #{age},</if>
</trim>

trim 标签

作用:最灵活的动态 SQL 标签,可以添加/删除前后缀

四个属性:
  prefix            → 在前面添加的内容
  prefixOverrides   → 去掉前面的指定内容
  suffix            → 在后面添加的内容
  suffixOverrides   → 去掉后面的指定内容
xml
<!-- 自定义 INSERT 语句 -->
<insert id="insertSelective">
    INSERT INTO user
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="name != null">name,</if>
        <if test="age != null">age,</if>
        <if test="email != null">email,</if>
    </trim>
    <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
        <if test="name != null">#{name},</if>
        <if test="age != null">#{age},</if>
        <if test="email != null">#{email},</if>
    </trim>
</insert>

2.4 foreach(遍历)

作用:遍历集合,常用于 IN 查询和批量操作

六个属性:
  collection  → 要遍历的集合(必填)
  item        → 每个元素的别名(必填)
  index       → 索引(可选)
  open        → 开始符号
  close       → 结束符号
  separator   → 分隔符
xml
<!-- ========== IN 查询 ========== -->
<select id="selectByIds" resultType="User">
    SELECT * FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
<!-- 结果:SELECT * FROM user WHERE id IN (1, 2, 3) -->

<!-- ========== 批量插入 ========== -->
<insert id="insertBatch">
    INSERT INTO user (name, age, email) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age}, #{user.email})
    </foreach>
</insert>
<!-- 结果:INSERT INTO user (name, age, email) VALUES (?, ?, ?), (?, ?, ?) -->

<!-- ========== 批量删除 ========== -->
<delete id="deleteByIds">
    DELETE FROM user WHERE id IN
    <foreach collection="list" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

collection 属性取值:

参数类型collection 值
Listlist(默认)或 @Param 指定的名字
Arrayarray(默认)或 @Param 指定的名字
Setcollection(默认)或 @Param 指定的名字
Map_parameter 或 key 名
java
// 传入 List
List<User> selectByIds(@Param("ids") List<Long> ids);
// XML: collection="ids"

// 传入数组
List<User> selectByIds(Long[] ids);
// XML: collection="array"

// 传入 Map 中的 List
Map<String, Object> params = new HashMap<>();
params.put("ids", Arrays.asList(1L, 2L, 3L));
// XML: collection="ids"

2.5 bind(变量绑定)

作用:创建变量,用于 SQL 中,解决更换数据库时的兼容问题
xml
<!-- 模糊查询:MySQL 用 CONCAT,Oracle 用 || -->
<select id="selectByName" resultType="User">
    <!-- 使用 bind 统一写法 -->
    <bind name="pattern" value="'%' + name + '%'"/>
    SELECT * FROM user WHERE name LIKE #{pattern}
</select>

<!-- 防止 SQL 注入的 LIKE 写法 -->
<select id="selectByNameSafe" resultType="User">
    <bind name="safeName" value="'%' + name + '%'"/>
    SELECT * FROM user WHERE name LIKE #{safeName}
</select>

三、实战场景

3.1 多条件查询

xml
<select id="selectByMultiCondition" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="minAge != null">
            AND age &gt;= #{minAge}
        </if>
        <if test="maxAge != null">
            AND age &lt;= #{maxAge}
        </if>
        <if test="email != null and email != ''">
            AND email = #{email}
        </if>
        <if test="createTimeStart != null">
            AND create_time &gt;= #{createTimeStart}
        </if>
        <if test="createTimeEnd != null">
            AND create_time &lt;= #{createTimeEnd}
        </if>
    </where>
    <if test="orderBy != null and orderBy != ''">
        ORDER BY ${orderBy} ${sortDirection}
    </if>
</select>

3.2 批量操作

xml
<!-- 批量插入 -->
<insert id="insertBatch">
    INSERT INTO user (name, age, email) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age}, #{user.email})
    </foreach>
</insert>

<!-- 批量更新(MySQL 特有语法) -->
<update id="updateBatch">
    <foreach collection="list" item="user" separator=";">
        UPDATE user
        SET name = #{user.name}, age = #{user.age}
        WHERE id = #{user.id}
    </foreach>
</update>

<!-- 批量更新(通用写法) -->
<update id="updateBatchByCase">
    UPDATE user
    <trim prefix="SET" suffixOverrides=",">
        <trim prefix="name = CASE" suffix="END,">
            <foreach collection="list" item="user">
                WHEN id = #{user.id} THEN #{user.name}
            </foreach>
        </trim>
        <trim prefix="age = CASE" suffix="END,">
            <foreach collection="list" item="user">
                WHEN id = #{user.id} THEN #{user.age}
            </foreach>
        </trim>
    </trim>
    WHERE id IN
    <foreach collection="list" item="user" open="(" separator="," close=")">
        #{user.id}
    </foreach>
</update>

3.3 动态排序

xml
<select id="selectByOrder" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null">AND name = #{name}</if>
    </where>
    <!-- 排序字段必须用 ${},因为表名/列名不能用占位符 -->
    <if test="orderBy != null and orderBy != ''">
        ORDER BY ${orderBy}
        <if test="sortDirection != null and sortDirection != ''">
            ${sortDirection}
        </if>
    </if>
</select>

注意: ${orderBy} 有 SQL 注入风险,必须在 Java 代码中做白名单校验。

四、OGNL 表达式

MyBatis 动态 SQL 中的 test 属性使用 OGNL 表达式

常用语法:
  - 基本:name != null, age > 18, status == 1
  - 字符串:name != null and name != ''
  - 集合:list != null and list.size() > 0
  - 三元:sex == 1 ? '男' : '女'
  - 方法调用:name.trim() != ''
  - 取反:!list.isEmpty()

五、速记总结

MyBatis 动态 SQL 六大标签:

  if                       → 条件判断,满足就拼接
  choose-when-otherwise    → 多选一,类似 switch
  where                    → 自动加 WHERE 并去除多余 AND/OR
  set                      → 自动加 SET 并去除多余逗号
  trim                     → 万能标签,自定义前后缀
  foreach                  → 遍历集合,IN 查询和批量操作
  bind                     → 变量绑定,解决模糊查询兼容

速记口诀:
  if 判断,choose 选,
  where 去 and,set 去逗号,
  trim 万能,foreach 遍历,
  bind 绑变量,模糊查询好。

六、面试要点

问题答案要点
动态 SQL 有哪些标签?if, choose, where, set, trim, foreach, bind
where 标签的作用?自动加 WHERE,去除多余 AND/OR
foreach 常用场景?IN 查询、批量插入、批量删除
#{ } 和 ${ } 在动态 SQL 中?排序用 ${},参数值用 #{}