MyBatis 动态 SQL 真正的全方位解析!!!!

一、动态 SQL 简介

1.1 什么是动态 SQL

动态 SQL 是 MyBatis 框架提供的一种强大的 SQL 构建机制,它允许开发者在 XML 映射文件或注解中,根据运行时的参数条件动态地构建 SQL 语句。这种机制通过一系列特殊的标签(如 if、choose、where、set 等)来实现条件判断和 SQL 片段拼接。

在实际开发中,动态 SQL 特别适用于以下典型场景:

  1. 多条件组合查询:例如用户管理系统的搜索功能,可能包含用户名模糊查询、状态筛选、注册时间范围等多个可选条件
  2. 批量操作:如根据传入的 ID 集合批量更新或删除记录
  3. 可选字段更新:只更新传入的非空字段,避免全字段覆盖

示例:一个简单的用户查询场景

<select id="findUsers" resultType="User">
  SELECT * FROM users
  <where>
    <if test="username != null">
      AND username LIKE CONCAT('%',#{username},'%')
    </if>
    <if test="status != null">
      AND status = #{status}
    </if>
  </where>
</select>

1.2 动态 SQL 的作用

1.2.1 减少代码冗余

传统 JDBC 开发中,要为不同的条件组合编写多个 SQL 语句变体。例如有 3 个可选查询条件时,理论上需要准备 8 种 SQL 组合(2^3)。而动态 SQL 通过条件标签只需维护一个基础 SQL 模板,由框架根据参数自动生成最终 SQL,显著减少了代码量。

1.2.2 提高 SQL 灵活性

动态 SQL 支持多种控制结构:

  • 条件分支:通过 <if><choose> 实现
  • 循环结构:通过 <foreach> 处理集合参数
  • SQL 片段:通过 <sql> 定义可重用的 SQL 块
  • 智能处理:<where> 自动处理前缀 AND/OR,<set> 处理更新语句中的逗号

1.2.3 增强 SQL 安全性

MyBatis 动态 SQL 采用预编译机制处理所有参数:

  1. 参数值都通过 #{} 语法绑定,防止 SQL 注入
  2. 自动处理特殊字符转义
  3. 类型安全校验,避免类型不匹配错误

对比示例:

// 不安全的字符串拼接(存在SQL注入风险)
String sql = "SELECT * FROM users WHERE id = " + userInput;

// 安全的使用方式(MyBatis自动防护)
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(@Param("id") Integer id);

此外,动态 SQL 还支持 OGNL 表达式,可以实现更复杂的条件判断逻辑,如:

<if test="@com.example.util.MyUtils@isValid(status)">
  AND status = #{status}
</if>

二、常用动态 SQL 标签详解

2.1 if 标签

if标签是 MyBatis 中最基础、最常用的动态 SQL 标签,它用于根据条件判断是否将其包含的 SQL 片段添加到最终生成的 SQL 语句中。

语法格式

<if test="条件表达式">
    SQL片段
</if>

其中,test属性用于指定条件表达式,表达式的结果为true时,会将if标签内的 SQL 片段拼接到主 SQL 中;结果为false时,则忽略该 SQL 片段。

代码示例:多条件查询

假设我们有一个用户表user,需要根据用户名(username)和年龄(age)进行多条件查询,当用户名或年龄为空时,忽略对应的查询条件。

Mapper 接口
public interface UserMapper {
    List<User> selectUserByCondition(User user);
}

XML 映射文件
<select id="selectUserByCondition" parameterType="com.example.entity.User" resultType="com.example.entity.User">
    SELECT id, username, age, email
    FROM user
    WHERE 1=1
    <if test="username != null and username != ''">
        AND username LIKE CONCAT('%', #{username}, '%')
    </if>
    <if test="age != null">
        AND age = #{age}
    </if>
</select>

详细说明:

  1. WHERE 1=1的作用是避免当所有if条件都不满足时,生成的 SQL 语句中出现多余的WHERE关键字
  2. username不为空且不为空字符串时,会添加用户名模糊查询的条件
  3. age不为空时,会添加年龄精确查询的条件
  4. 使用CONCAT函数实现模糊查询,兼容不同数据库的语法差异

应用场景:

  • 后台管理系统的用户查询功能
  • 数据报表的筛选条件
  • API接口中的可选参数查询

2.2 where 标签

在使用if标签进行多条件查询时,我们通常需要在WHERE子句后添加1=1来避免 SQL 语法错误。而where标签可以自动处理这种情况,它会智能地判断是否需要添加WHERE关键字,并且会去除多余的ANDOR关键字。

语法格式

<where>
    <if test="条件表达式1">
        AND/OR SQL片段1
    </if>
    <if test="条件表达式2">
        AND/OR SQL片段2
    </if>
    ...
</where>

代码示例:优化多条件查询

使用where标签对上面的多条件查询示例进行优化:

XML 映射文件
<select id="selectUserByCondition" parameterType="com.example.entity.User" resultType="com.example.entity.User">
    SELECT id, username, age, email
    FROM user
    <where>
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>

优化点说明:

  1. 移除了WHERE 1=1的固定条件
  2. where标签会自动处理以下情况:
    • 当所有if条件都不满足时,不会生成WHERE关键字
    • 如果有条件满足,会自动添加WHERE关键字
    • 会智能去除第一个 SQL 片段前多余的ANDOR

注意事项:

  • 每个条件语句前仍需保留ANDOR
  • where标签只能用于WHERE子句部分
  • 适用于SELECTUPDATEDELETE等需要WHERE条件的语句

2.3 choose、when、otherwise 标签

choosewhenotherwise标签组合使用,类似于 Java 中的if-else if-else语句,用于实现多条件分支判断。choose标签是父标签,when标签用于指定分支条件,otherwise标签用于指定默认分支(当所有when条件都不满足时执行)。

语法格式

<choose>
    <when test="条件表达式1">
        SQL片段1
    </when>
    <when test="条件表达式2">
        SQL片段2
    </when>
    ...
    <otherwise>
        SQL片段n(默认分支)
    </otherwise>
</choose>

代码示例:按优先级查询

假设我们需要查询用户信息,查询优先级为:先根据用户 ID(id)查询,如果 ID 为空,则根据用户名(username)查询,如果用户名也为空,则查询年龄大于 18 岁的用户。

XML 映射文件
<select id="selectUserByPriority" parameterType="com.example.entity.User" resultType="com.example.entity.User">
    SELECT id, username, age, email
    FROM user
    <where>
        <choose>
            <when test="id != null">
                id = #{id}
            </when>
            <when test="username != null and username != ''">
                username LIKE CONCAT('%', #{username}, '%')
            </when>
            <otherwise>
                age > 18
            </otherwise>
        </choose>
    </where>
</select>

执行逻辑说明:

  1. 会按照when标签的顺序依次判断条件
  2. 只要有一个when条件满足,就会执行对应的 SQL 片段,并忽略后续的whenotherwise标签
  3. 如果所有when条件都不满足,则执行otherwise标签内的 SQL 片段
  4. if标签不同,choose结构只会选择一个分支执行

典型应用场景:

  • 多级查询优先级设置
  • 条件互斥的查询需求
  • 默认查询条件的设置

2.4 set 标签

set标签主要用于更新操作(UPDATE语句),它可以自动处理 SQL 语句中多余的逗号(,),并智能地添加SET关键字。

语法格式

<update id="updateUser" parameterType="com.example.entity.User">
    UPDATE user
    <set>
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
        <if test="email != null and email != ''">
            email = #{email}
        </if>
    </set>
    WHERE id = #{id}
</update>

代码示例:动态更新用户信息

假设我们需要更新用户信息,只更新不为空的字段。使用set标签可以避免因某些字段为空而导致的 SQL 语法错误。

Mapper 接口
public interface UserMapper {
    int updateUser(User user);
}

XML 映射文件
<update id="updateUser" parameterType="com.example.entity.User">
    UPDATE user
    <set>
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
        <if test="email != null and email != ''">
            email = #{email}
        </if>
    </set>
    WHERE id = #{id}
</update>

功能特点:

  1. 自动添加SET关键字
  2. 智能去除最后一个 SQL 片段后的多余逗号
  3. 如果所有if条件都不满足,会生成错误的 SQL(UPDATE user SET WHERE id = ?
  4. 在实际使用中,需要确保至少有一个字段需要更新,或者在代码中进行判断

最佳实践建议:

  1. 在业务层先检查更新字段是否为空
  2. 对于必填字段,不要放在set标签内
  3. 可以结合<trim>标签实现更复杂的更新逻辑
  4. 适用于部分更新、选择性更新的场景

常见应用:

  • 用户信息修改功能
  • 数据状态更新
  • 批量字段更新操作

2.5 foreach 标签详解

foreach标签是MyBatis中用于循环遍历集合或数组的强大标签,它在处理批量操作时特别有用。通过foreach标签,我们可以轻松实现IN查询、批量插入、批量更新等常见数据库操作。

语法格式详解

<foreach collection="集合/数组参数名" 
         item="遍历元素别名" 
         index="索引别名" 
         open="开始符号" 
         close="结束符号" 
         separator="分隔符">
    #{遍历元素别名}
</foreach>

属性详细说明

  1. collection

    • 指定要遍历的集合或数组的参数名
    • 特殊默认值:
      • List集合:默认参数名为"list"
      • 数组:默认参数名为"array"
      • Map集合或自定义对象中的集合属性:直接指定属性名
    • 示例:collection="idList"collection="array"
  2. item

    • 定义遍历过程中当前元素的引用名称
    • 在标签内部通过#{别名}引用元素值
    • 示例:item="id",则在循环体内使用#{id}
  3. index(可选):

    • 对于List:表示当前元素的索引(从0开始)
    • 对于Map:表示键(key)
    • 对于数组:表示数组下标
    • 示例:index="i",可用于动态生成字段名
  4. open(可选):

    • 循环开始前拼接的字符串
    • 示例:open="("会在循环前添加左括号
  5. close(可选):

    • 循环结束后拼接的字符串
    • 示例:close=")"会在循环后添加右括号
  6. separator(可选):

    • 元素之间的分隔符
    • 示例:separator=","会在每个元素间添加逗号

应用场景与代码示例

场景1:批量查询(IN查询)

当需要根据多个ID查询用户信息时:

// Mapper接口
public interface UserMapper {
    List<User> selectUserByIds(@Param("ids") List<Integer> idList);
}

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

执行时若传入ids=[1,2,3],生成的SQL为:

SELECT * FROM user WHERE id IN (1,2,3)

场景2:批量插入

一次性插入多条用户记录:

// Mapper接口
public interface UserMapper {
    int batchInsert(@Param("users") List<User> userList);
}

<insert id="batchInsert">
    INSERT INTO user (name, age) VALUES
    <foreach collection="users" item="user" separator=",">
        (#{user.name}, #{user.age})
    </foreach>
</insert>

传入两个用户对象时,生成的SQL:

INSERT INTO user (name, age) VALUES 
('张三', 20), 
('李四', 22)

场景3:批量更新

使用CASE WHEN实现批量更新:

<update id="batchUpdate">
    UPDATE user 
    SET name = CASE id
        <foreach collection="users" item="user">
            WHEN #{user.id} THEN #{user.name}
        </foreach>
    END
    WHERE id IN
    <foreach collection="users" item="user" open="(" close=")" separator=",">
        #{user.id}
    </foreach>
</update>

注意事项

  1. 大数据量时(超过1000条),应考虑分批处理
  2. Oracle数据库IN查询参数超过1000个时会报错
  3. 使用@Param注解可以自定义collection属性值
  4. 嵌套foreach时要注意item命名冲突

2.6 trim 标签详解

trim标签提供了强大的SQL片段处理能力,可以灵活控制SQL语句的拼接方式。它实际上是where和set标签的底层实现。

语法结构

<trim prefix="前缀" 
      suffix="后缀" 
      prefixOverrides="需要去除的前缀" 
      suffixOverrides="需要去除的后缀">
    SQL片段
</trim>

属性精解

  1. prefix

    • 在整个trim内容前添加的字符串
    • 示例:prefix="WHERE"会添加WHERE关键字
  2. suffix

    • 在整个trim内容后添加的字符串
    • 示例:suffix=";"会添加分号
  3. prefixOverrides

    • 去除内容开头指定的字符串(多个用|分隔)
    • 示例:prefixOverrides="AND|OR"会去除开头多余的AND或OR
  4. suffixOverrides

    • 去除内容结尾指定的字符串
    • 示例:suffixOverrides=","会去除末尾多余的逗号

实际应用示例

示例1:自定义WHERE条件

实现类似where标签的功能:

<select id="findUsers" resultType="User">
    SELECT * FROM user
    <trim prefix="WHERE" prefixOverrides="AND|OR">
        <if test="name != null">
            AND name = #{name}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </trim>
</select>

执行逻辑:

  1. 当有条件成立时,添加WHERE关键字
  2. 自动去除第一个条件前多余的AND/OR
  3. 若无任何条件,WHERE关键字也不会出现

示例2:动态UPDATE语句

替代set标签的实现:

<update id="updateUser">
    UPDATE user
    <trim prefix="SET" suffixOverrides=",">
        <if test="name != null">
            name = #{name},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
    </trim>
    WHERE id = #{id}
</update>

特点:

  1. 自动添加SET关键字
  2. 智能去除最后一个多余的逗号
  3. 若无任何更新字段,SET不会出现

示例3:复杂条件拼接

实现多条件组合查询:

<select id="search" resultType="User">
    SELECT * FROM user
    <trim prefix="WHERE" prefixOverrides="AND|OR" suffixOverrides="AND|OR">
        <if test="param1 != null">
            (col1 = #{param1} OR col2 = #{param1}) AND
        </if>
        <if test="param2 != null">
            col3 = #{param2} OR
        </if>
    </trim>
</select>

优势:

  1. 同时处理前缀和后缀的多余关键字
  2. 支持复杂的条件组合
  3. 自动处理边界情况

高级技巧

  1. 嵌套使用

    <trim prefix="(" suffix=")" prefixOverrides="AND|OR">
        <trim prefixOverrides="AND|OR">
            <!-- 条件内容 -->
        </trim>
    </trim>
    

  2. 动态表名

    <trim prefix="FROM" prefixOverrides=",">
        <if test="table1">, table1</if>
        <if test="table2">, table2</if>
    </trim>
    

  3. 批量操作组合

    <insert id="batchInsert">
        <trim prefix="INSERT ALL" suffix="SELECT 1 FROM DUAL">
            <foreach collection="list" item="item">
                INTO table VALUES (#{item.val1}, #{item.val2})
            </foreach>
        </trim>
    </insert>
    

通过灵活组合这些属性,trim标签几乎可以满足所有动态SQL拼接的需求,是MyBatis动态SQL中最强大的基础标签之一。

三、动态 SQL 的高级用法

3.1 结合 OGNL 表达式

MyBatis 动态 SQL 的 test 属性支持 OGNL(Object-Graph Navigation Language)表达式,这是一种功能强大的表达式语言,可以通过对象图导航的方式进行复杂条件判断。OGNL 表达式在 MyBatis 中的应用使得动态 SQL 的条件判断更加灵活和强大,能够处理各种复杂的业务场景。

示例 1:判断集合是否为空
<if test="userList != null and userList.size() > 0">
    <!-- 集合不为空时执行的SQL片段 -->
    AND user_id IN
    <foreach collection="userList" item="userId" open="(" separator="," close=")">
        #{userId}
    </foreach>
</if>

说明

  • userList != null 确保集合对象不为空。
  • userList.size() > 0 确保集合中至少有一个元素。
  • 结合 foreach 标签,可以动态生成 IN 子句,适用于批量查询场景。
示例 2:判断字符串是否包含指定字符
<if test="username != null and username.contains('zhang')">
    AND username LIKE CONCAT('%', #{username}, '%')
</if>

说明

  • username.contains('zhang') 使用 OGNL 的字符串方法判断是否包含子串。
  • 适用于模糊查询场景,比如搜索用户名中包含特定关键字的用户。
示例 3:判断参数是否为指定枚举值

假设我们有一个枚举类 UserStatus,用于表示用户状态:

public enum UserStatus {
    ACTIVE, INACTIVE, LOCKED
}

在动态 SQL 中判断用户状态是否为 ACTIVE

<if test="status == @com.example.enums.UserStatus@ACTIVE">
    AND status = #{status}
</if>

说明

  • @com.example.enums.UserStatus@ACTIVE 是 OGNL 的枚举值引用语法,通过全限定类名访问枚举值。
  • 适用于状态过滤的场景,比如只查询活跃用户。

3.2 动态 SQL 与 SQL 片段(sql 标签)结合

当多个 SQL 语句中包含相同的 SQL 片段时,可以使用 <sql> 标签将其抽取为公共片段,然后通过 <include> 标签引用。这种方式不仅提高了代码的复用性,还能减少重复代码,便于维护。

代码示例

抽取公共 SQL 片段

<sql id="userColumns">
    id, username, age, email, create_time, update_time
</sql>

说明

  • id 是片段的唯一标识,用于后续引用。
  • 片段中可以包含任意合法的 SQL 部分,比如字段列表、JOIN 语句等。

引用公共 SQL 片段

<select id="selectUserById" parameterType="java.lang.Integer" resultType="com.example.entity.User">
    SELECT <include refid="userColumns"/>
    FROM user
    WHERE id = #{id}
</select>

<select id="selectAllUser" resultType="com.example.entity.User">
    SELECT <include refid="userColumns"/>
    FROM user
</select>

<select id="selectUserByCondition" parameterType="map" resultType="com.example.entity.User">
    SELECT <include refid="userColumns"/>
    FROM user
    <where>
        <if test="username != null">
            AND username = #{username}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>

说明

  • <include refid="userColumns"/> 通过 refid 引用之前定义的 SQL 片段。
  • 适用于多表查询时字段列表较长的情况,或者需要统一字段定义的场景。
  • 如果字段列表需要修改,只需修改 sql 标签中的内容,所有引用该片段的地方都会自动更新。

高级用法:带参数的 SQL 片段

<sql id="orderByClause">
    ORDER BY ${orderByField} ${orderByDirection}
</sql>

<select id="selectUserWithOrder" parameterType="map" resultType="com.example.entity.User">
    SELECT <include refid="userColumns"/>
    FROM user
    <include refid="orderByClause"/>
</select>

说明

  • ${orderByField}${orderByDirection} 是动态参数,使用时需确保参数安全。
  • 适用于需要动态排序的场景,比如表格列排序功能。

四、动态 SQL 使用注意事项

  1. 参数类型处理:在MyBatis动态SQL的test属性中进行参数判断时,必须充分考虑参数类型特性。针对字符串类型参数,需要进行双重判断:首先检查是否为null,其次检查是否为空字符串(test="username != null and username != ''")。这种处理方式的原因是:

    • null表示参数未被赋值
    • 空字符串('')表示参数已被赋值为空值 对于数值类型(如Integer、Long等),则只需进行null判断(test="age != null"),因为数值类型不存在空字符串的概念,如果误加空字符串判断(如test="age != ''")反而会导致表达式求值错误。例如:
    <!-- 正确示例 -->
    <if test="username != null and username != ''">AND user_name = #{username}</if>
    <if test="age != null">AND user_age = #{age}</if>
    
    <!-- 错误示例 -->
    <if test="age != null and age != ''">...</if> <!-- 数值类型不应判断空字符串 -->
    

  2. SQL注入防护:MyBatis的动态SQL机制虽然提供了基础的SQL注入防护,但仍有需要特别注意的情况:

    • 使用#符号时(如#{param}),MyBatis会使用预编译语句,自动进行参数转义
    • 使用$符号直接拼接时(如ORDER BY ${column}),存在SQL注入漏洞 安全实践建议:
    • 90%以上的场景都应该使用#符号
    • 必须使用$符号的场景(如动态表名、动态排序字段):
      • 应该限制参数值为白名单值(如:test="orderBy == 'create_time' or orderBy == 'update_time'")
      • 或对参数值进行严格校验(如只允许字母、数字和下划线) 典型危险场景示例:
    <!-- 危险示例:直接拼接用户输入 -->
    ORDER BY ${userInput}
    
    <!-- 安全示例:使用白名单控制 -->
    <choose>
      <when test="orderBy == 'name'">ORDER BY user_name</when>
      <when test="orderBy == 'age'">ORDER BY user_age</when>
      <otherwise>ORDER BY create_time</otherwise>
    </choose>
    

  3. 避免过度复杂的动态SQL:动态SQL虽然灵活,但过度使用会导致:

    • 可读性下降:嵌套多层<if>/<choose>标签
    • 维护困难:业务逻辑分散在SQL和Java代码中
    • 性能问题:可能导致执行计划不稳定 改进方案:
    • 将复杂查询拆分为多个简单查询,在Java层组装
    • 使用SQL视图或存储过程封装复杂逻辑
    • 保持单个动态SQL块不超过3级嵌套 反面教材示例:
    <!-- 过度复杂的动态SQL -->
    <select id="findUsers" resultType="User">
      SELECT * FROM users
      <where>
        <if test="param1 != null">
          AND field1 = #{param1}
          <if test="param2 != null">
            OR field2 = #{param2}
            <choose>
              <when test="param3 == 'A'">...</when>
              <otherwise>...</otherwise>
            </choose>
          </if>
        </if>
        <!-- 更多嵌套... -->
      </where>
    </select>
    

  4. 批量操作的性能考量:使用foreach标签进行批量操作时需注意:

    • 数据库限制:
      • MySQL默认max_allowed_packet=4MB
      • Oracle有SQL语句长度限制
      • SQL Server有参数个数限制(约2100个)
    • 性能优化方案:
      1. 分批处理:每批500-1000条记录
      // Java层分批示例
      List<User> users = getUsers();
      int batchSize = 500;
      for (int i = 0; i < users.size(); i += batchSize) {
          List<User> batch = users.subList(i, Math.min(i + batchSize, users.size()));
          userMapper.batchInsert(batch);
      }
      

      1. 调整数据库配置(需DBA配合)
      2. 使用JDBC批处理模式
      3. 考虑使用LOAD DATA INFILE(MySQL)等批量导入工具
  5. 参数传递的一致性:参数命名必须严格匹配,注意:

    • Map传参:键名区分大小写
    • 对象传参:属性名区分大小写
    • @Param注解:指定的别名优先级最高 常见问题场景:
    // Java代码
    Map<String, Object> params = new HashMap<>();
    params.put("userName", "John");  // 注意大小写
    params.put("USER_AGE", 25);     // 全大写
    
    // 错误示例:大小写不匹配
    <if test="username != null">...</if>  <!-- 应为userName -->
    <if test="user_age != null">...</if>  <!-- 应为USER_AGE -->
    
    // 最佳实践:
    - 统一命名规范(如全小写下划线)
    - 使用@Param明确指定参数名
    @Select("...")
    List<User> findUsers(@Param("user_name") String userName);
    
    <!-- 对应XML -->
    <if test="user_name != null">...</if>
    

五、动态 SQL 在注解方式中的应用

5.1 注解方式的核心原理

通过注解指定一个 "SQL 提供者" 类,该类中包含生成 SQL 语句的静态方法(或非静态方法),MyBatis 会在执行 SQL 时调用该方法获取动态生成的 SQL 语句。这种机制的核心在于:

  1. 运行时动态构建:SQL 语句不是在编译时确定的,而是在运行时根据参数动态生成
  2. 类型安全:使用 Java 代码构建 SQL,避免了字符串拼接的错误
  3. 灵活控制:可以利用 Java 的所有控制结构(条件判断、循环等)构建复杂的 SQL 逻辑

MyBatis 通过 @SelectProvider@InsertProvider@UpdateProvider@DeleteProvider 等注解来支持这种动态 SQL 生成方式。

5.2 代码示例:注解方式实现多条件查询

步骤 1:定义 SQL 提供者类

public class UserSqlProvider {
    /**
     * 多条件查询SQL生成方法
     * @param user 查询条件对象
     * @return 动态生成的SQL语句
     */
    public String selectUserByCondition(User user) {
        // 使用MyBatis提供的SQL工具类构建SQL
        return new SQL() {{
            SELECT("id, username, age, email");
            FROM("user");
            
            // 动态添加用户名查询条件
            if (user.getUsername() != null && !"".equals(user.getUsername())) {
                WHERE("username LIKE CONCAT('%', #{username}, '%')");
            }
            
            // 动态添加年龄查询条件
            if (user.getAge() != null) {
                WHERE("age = #{age}");
            }
            
            // 可以继续添加更多条件
            if (user.getEmail() != null) {
                WHERE("email = #{email}");
            }
            
            // 可以添加排序条件
            ORDER_BY("id DESC");
        }}.toString();
    }
}

上述代码中:

  • SQL 是 MyBatis 提供的工具类,通过方法链方式构建 SQL
  • 使用 {{...}} 双括号语法初始化匿名内部类
  • 条件判断完全基于 Java 语法,非常灵活
  • 可以添加各种子句(WHERE、ORDER BY 等)

步骤 2:在 Mapper 接口中使用注解引用 SQL 提供者

public interface UserMapper {
    /**
     * 多条件查询用户
     * @param user 包含查询条件的用户对象
     * @return 匹配的用户列表
     */
    @SelectProvider(
        type = UserSqlProvider.class,  // 指定SQL提供者类
        method = "selectUserByCondition"  // 指定生成SQL的方法名
    )
    List<User> selectUserByCondition(User user);
}

注解属性说明:

  • type:指定 SQL 提供者类的 Class 对象
  • method:指定 SQL 提供者类中生成 SQL 的方法名
    • MyBatis 3.5+ 版本支持非静态方法
    • 方法必须返回 String 类型的 SQL 语句

5.3 注解方式与 XML 方式的对比

对比维度注解方式XML 方式
可读性复杂 SQL 逻辑在 Java 代码中,结构清晰,IDE 支持好(代码提示、跳转)SQL 片段与动态标签混合,直观性强,但复杂逻辑可能难以理解
维护性适合复杂逻辑(如多分支、循环),便于调试,修改需要重新编译适合简单 SQL,修改无需重新编译,可直接热加载
灵活性支持 Java 代码的所有逻辑(如循环、判断),可以调用其他工具方法依赖 MyBatis 提供的动态标签(if/choose/foreach等),逻辑受限
性能运行时动态生成 SQL,可能有轻微性能开销XML 解析后缓存,性能较好
适用场景复杂动态 SQL、团队偏好注解开发、需要复杂逻辑控制简单/中等复杂度 SQL、需要频繁修改 SQL 的场景、大型项目团队协作
SQL 复用可通过 Java 方法复用 SQL 片段可通过 <sql> 标签和 <include> 标签复用 SQL 片段

实际应用建议

  1. 对于简单固定的 SQL,使用 XML 方式更直观
  2. 对于需要复杂逻辑(如多重条件判断、循环等)的动态 SQL,推荐使用注解方式
  3. 大型项目中可以混合使用两种方式,根据具体场景选择

六、动态 SQL 常见问题与解决方案

6.1 问题 1:foreach标签遍历集合时提示 "Parameter 'list' not found"

问题描述

在使用MyBatis的foreach标签遍历List集合时,可能会遇到以下错误提示:

org.apache.ibatis.binding.BindingException: Parameter 'list' not found. Available parameters are [arg0, collection, list]

原因分析

  1. 参数命名问题

    • 当Mapper接口方法的参数为单个List集合且未使用@Param注解时,MyBatis 3.4.6及以下版本默认将参数名识别为"list"
    • 但在3.5.0及以上版本中,参数名可能被识别为"collection"或"arg0"
  2. @Param注解使用不当

    • 如果使用了@Param注解指定了参数名(如@Param("idList")),则必须确保foreach标签的collection属性值与该注解指定的参数名完全一致

解决方案

方案 1:显式使用@Param注解
// Mapper接口方法
List<User> selectUserByIds(@Param("idList") List<Integer> idList);

<!-- XML映射文件 -->
<select id="selectUserByIds" resultType="User">
  SELECT * FROM user WHERE id IN
  <foreach collection="idList" item="id" open="(" close=")" separator=",">
    #{id}
  </foreach>
</select>

方案 2:使用默认参数名
// 不使用@Param注解
List<User> selectUserByIds(List<Integer> ids);

<!-- 根据MyBatis版本选择正确的参数名 -->
<foreach collection="list" item="id" ...>  <!-- 3.4.6及以下版本 -->
<foreach collection="collection" item="id" ...>  <!-- 3.5.0及以上版本 -->

6.2 问题 2:动态更新时所有字段为空导致SQL语法错误

问题描述

使用set标签动态更新时,如果传入对象的所有字段都为空,会生成如下错误SQL:

UPDATE user SET WHERE id = ?

原因分析

  1. 动态SQL生成机制
    • set标签会生成SET关键字
    • 只有当if条件满足时才会生成对应的字段赋值语句
    • 当所有if条件都不满足时,SET后为空,导致语法错误

解决方案

方案 1:业务层校验
public void updateUser(User user) {
    // 检查至少一个字段不为null或空字符串
    if (Stream.of(
        user.getUsername(),
        user.getAge(),
        user.getEmail()
    ).allMatch(value -> value == null || (value instanceof String && ((String)value).isEmpty()))) {
        throw new BusinessException("至少需要更新一个字段");
    }
    userMapper.updateUser(user);
}

方案 2:SQL层处理(谨慎使用)
<update id="updateUser">
  UPDATE user
  <set>
    <if test="username != null and username != ''">username = #{username},</if>
    <if test="age != null">age = #{age},</if>
    <if test="email != null and email != ''">email = #{email},</if>
    <!-- 默认更新一个无影响字段 -->
    <if test="true">update_time = NOW()</if>
  </set>
  WHERE id = #{id}
</update>

6.3 问题 3:OGNL表达式判断字符串相等时失效

问题描述

在test属性中判断字符串相等时,如下条件始终返回false:

<if test="status == 'ACTIVE'">

原因分析

  1. OGNL类型处理
    • 单引号在OGNL中表示char类型
    • 当比较字符串时,会导致类型不匹配
    • 例如:'ACTIVE'实际上被解释为char[],而不是String

解决方案

方案 1:正确使用引号
<!-- 使用双引号包裹字符串 -->
<if test='status == "ACTIVE"'>

<!-- 或者嵌套引号 -->
<if test="status == 'ACTIVE'.toString()">

方案 2:使用equals方法(推荐)
<if test="status != null and status.equals('ACTIVE')">
  
<!-- 或使用Java 7+的语法糖 -->
<if test='"ACTIVE".equals(status)'>

方案 3:自定义判断方法
// 在工具类中定义方法
public static boolean isActive(String status) {
    return "ACTIVE".equals(status);
}
<if test="@com.example.util.MyBatisUtils@isActive(status)">

七、动态 SQL 实战案例:多条件分页查询

7.1 步骤 1:引入分页插件依赖(以 Maven 为例)

PageHelper 是一个流行的 MyBatis 分页插件,它能简化分页查询的实现过程。在项目的 pom.xml 文件中添加以下依赖:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.6</version>
</dependency>

注意事项

  • 确保版本号与你的 Spring Boot 版本兼容
  • 对于非 Spring Boot 项目,可以使用 pagehelper 核心依赖
  • 该依赖会自动配置拦截器,无需额外配置

7.2 步骤 2:定义查询条件实体类

创建一个数据传输对象(DTO)来封装查询条件和分页参数:

public class UserQueryDTO {
    // 分页参数
    private Integer pageNum = 1;  // 默认第一页
    private Integer pageSize = 10;  // 默认每页10条
    
    // 查询条件
    private String username;  // 用户名模糊查询
    private Integer age;      // 精确年龄查询
    private String email;     // 邮箱模糊查询
    
    // 省略getter和setter方法
    // 实际开发中建议使用Lombok的@Data注解
}

设计说明

  • 分页参数设置默认值可提高接口健壮性
  • 查询条件字段可根据业务需求扩展
  • 建议使用Java Bean验证注解(@NotNull, @Size等)进行参数校验

7.3 步骤 3:编写 Mapper 接口和 XML 映射文件

Mapper 接口定义

public interface UserMapper {
    /**
     * 多条件分页查询用户列表
     * @param queryDTO 查询条件对象
     * @return 用户列表(不包含分页信息)
     */
    List<User> selectUserByPage(UserQueryDTO queryDTO);
}

XML 映射文件实现

<select id="selectUserByPage" parameterType="com.example.dto.UserQueryDTO" 
        resultType="com.example.entity.User">
    SELECT id, username, age, email
    FROM user
    <where>
        <!-- 用户名模糊查询(当username不为空时生效) -->
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        
        <!-- 精确年龄查询 -->
        <if test="age != null">
            AND age = #{age}
        </if>
        
        <!-- 邮箱模糊查询 -->
        <if test="email != null and email != ''">
            AND email LIKE CONCAT('%', #{email}, '%')
        </if>
    </where>
    <!-- 注意:这里不需要手动添加LIMIT语句,分页插件会自动处理 -->
</select>

动态SQL特点

  • <where>标签会自动处理WHERE关键字和去除多余的AND
  • <if>标签实现条件动态拼接
  • CONCAT函数确保模糊查询的正确格式

7.4 步骤 4:编写 Service 层代码(集成分页插件)

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    /**
     * 分页查询用户列表
     * @param queryDTO 查询条件
     * @return 包含分页信息的查询结果
     */
    public PageInfo<User> selectUserByPage(UserQueryDTO queryDTO) {
        // 开启分页(必须放在查询方法前)
        PageHelper.startPage(queryDTO.getPageNum(), queryDTO.getPageSize());
        
        // 执行查询(此时SQL已被分页插件拦截并修改)
        List<User> userList = userMapper.selectUserByPage(queryDTO);
        
        // 封装分页结果(包含总条数、总页数、当前页数据等)
        return new PageInfo<>(userList);
    }
}

关键点说明

  1. PageHelper.startPage() 必须放在查询方法前
  2. 分页插件通过线程绑定方式传递分页参数
  3. PageInfo 提供丰富的分页信息:
    • 当前页码(pageNum)
    • 每页条数(pageSize)
    • 总页数(pages)
    • 总记录数(total)
    • 当前页数据(list)

7.5 步骤 5:测试分页查询

@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;

    @Test
    public void testSelectUserByPage() {
        // 构建查询条件
        UserQueryDTO queryDTO = new UserQueryDTO();
        queryDTO.setPageNum(1);        // 查询第1页
        queryDTO.setPageSize(10);      // 每页10条记录
        queryDTO.setUsername("zhang"); // 用户名包含"zhang"
        
        // 执行分页查询
        PageInfo<User> pageInfo = userService.selectUserByPage(queryDTO);
        
        // 验证结果
        System.out.println("总条数:" + pageInfo.getTotal());
        System.out.println("总页数:" + pageInfo.getPages());
        System.out.println("当前页数据量:" + pageInfo.getList().size());
        System.out.println("当前页数据:" + pageInfo.getList());
        
        // 可选择断言验证
        assertTrue(pageInfo.getList().size() <= 10);
    }
}

测试要点

  • 验证分页参数是否正确应用
  • 检查条件查询是否生效
  • 确认分页信息计算是否正确
  • 边界测试:第一页、最后一页、空结果等情况
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值