MyBatis 动态 SQL 详解:优雅处理复杂查询场景

一、动态 SQL 概述:为什么需要动态 SQL?

在实际开发中,数据库查询往往不是固定不变的,而是需要根据不同条件动态生成 SQL 语句。例如:

  • 多条件查询时,用户可能输入部分条件或全部条件
  • 数据更新时,可能只更新部分字段
  • 批量操作时,需要根据集合长度生成对应 SQL

如果手动拼接 SQL 字符串,会面临以下问题:

  • 繁琐的字符串拼接逻辑
  • 容易出现语法错误(如多余的 AND/OR)
  • 代码可读性和可维护性差
  • 潜在的 SQL 注入风险

MyBatis 的动态 SQL 功能完美解决了这些问题,它允许在 XML 映射文件中使用标签编写动态逻辑,自动生成符合条件的 SQL 语句。本文将全面讲解 MyBatis 动态 SQL 的常用标签和实战场景。

二、if 标签:条件判断的基础

if标签是动态 SQL 中最基本的条件判断标签,用于根据参数值决定是否拼接某段 SQL。

2.1 基本用法

Mapper 接口

java

运行

// 多条件查询
List<User> queryUserByCondition(@Param("username") String username,
                               @Param("age") Integer age,
                               @Param("email") String email);

映射文件

xml

<select id="queryUserByCondition" resultType="User">
    SELECT * 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>
    <if test="email != null and email != ''">
        AND email = #{email}
    </if>
</select>

说明

  • test属性:判断条件表达式
  • 当条件为true时,拼接标签内的 SQL 片段
  • 使用WHERE 1=1避免条件都不满足时的 SQL 语法错误

2.2 常见判断条件

条件表达式
非空判断test="param != null"
字符串非空且非空串test="param != null and param != ''"
集合非空test="list != null and list.size() > 0"
数值比较test="age > 18"
字符串包含test="username.indexOf('张') != -1"
多条件或test="age > 18 or score > 90"

2.3 注意事项

  • 字符串判断需同时检查null和空串
  • 数值类型只需检查null(空串会转换为 0)
  • 条件表达式中使用and/or而非&&/||
  • 字符串比较使用==而非equals()方法

三、where/trim 标签:智能处理条件拼接

where标签用于自动处理条件拼接中的AND/OR关键字,替代传统的WHERE 1=1写法。

3.1 where 标签用法

xml

<select id="queryUserByCondition" resultType="User">
    SELECT * FROM user
    <where>
        <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 = #{email}
        </if>
    </where>
</select>

where 标签的作用

  • 当内部有条件成立时,自动添加WHERE关键字
  • 自动去除条件片段开头的ANDOR
  • 当所有条件都不成立时,不添加WHERE关键字

3.2 trim 标签:更灵活的条件处理

trim标签通过属性配置,可以实现where标签的功能,甚至更多:

xml

<select id="queryUserByCondition" resultType="User">
    SELECT * FROM user
    <trim prefix="WHERE" prefixOverrides="AND | OR">
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </trim>
</select>

trim 标签属性

  • prefix:拼接在 trim 内容前的字符串
  • suffix:拼接在 trim 内容后的字符串
  • prefixOverrides:需要从内容开头去除的字符串
  • suffixOverrides:需要从内容结尾去除的字符串

四、set 标签:动态更新字段

set标签用于动态更新场景,自动处理字段后的逗号。

4.1 基本用法

Mapper 接口

java

运行

// 动态更新用户信息
int updateUserSelective(User user);

映射文件

xml

<update id="updateUserSelective" parameterType="User">
    UPDATE user
    <set>
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="password != null and password != ''">
            password = #{password},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
        <if test="email != null and email != ''">
            email = #{email}
        </if>
    </set>
    WHERE id = #{id}
</update>

set 标签的作用

  • 自动添加SET关键字
  • 自动去除字段后的逗号
  • 当没有字段需要更新时,会导致 SQL 错误(需避免)

4.2 使用 trim 模拟 set 标签

xml

<trim prefix="SET" suffixOverrides=",">
    <if test="username != null">username = #{username},</if>
    <if test="age != null">age = #{age},</if>
</trim>

五、foreach 标签:集合迭代处理

foreach标签用于遍历集合或数组,常用于IN查询和批量操作。

5.1 基本用法(IN 查询)

Mapper 接口

java

运行

// 批量查询
List<User> getUserByIds(@Param("ids") List<Integer> ids);

映射文件

xml

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

5.2 批量插入

Mapper 接口

java

运行

// 批量插入
int batchInsert(@Param("users") List<User> users);

映射文件

xml

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

5.3 批量更新(MySQL)

xml

<update id="batchUpdate">
    <foreach collection="users" item="user" separator=";">
        UPDATE user
        SET username = #{user.username},
            age = #{user.age}
        WHERE id = #{user.id}
    </foreach>
</update>

注意:MySQL 需要开启多语句执行支持(在 URL 后添加allowMultiQueries=true

5.4 foreach 标签属性详解

属性作用
collection集合参数名称(必填)
item迭代变量名(必填)
index索引变量名(可选)
open拼接在迭代内容前的字符串
close拼接在迭代内容后的字符串
separator元素间的分隔符
itemType集合元素类型(可选)

六、choose/when/otherwise 标签:多条件分支

choose标签类似 Java 中的switch语句,只执行第一个满足条件的when分支。

6.1 基本用法

Mapper 接口

java

运行

// 复杂条件查询
List<User> queryUserByChoose(@Param("username") String username,
                            @Param("age") Integer age,
                            @Param("email") String email);

映射文件

xml

<select id="queryUserByChoose" resultType="User">
    SELECT * FROM user
    <where>
        <choose>
            <when test="username != null and username != ''">
                AND username LIKE CONCAT('%', #{username}, '%')
            </when>
            <when test="email != null and email != ''">
                AND email = #{email}
            </when>
            <otherwise>
                AND age > #{age}
            </otherwise>
        </choose>
    </where>
</select>

说明

  • choose:包裹所有条件分支
  • when:条件分支,类似case
  • otherwise:默认分支,类似default
  • 只会执行第一个满足条件的when,其他分支会被忽略

七、bind 标签:创建可复用的变量

bind标签用于在 SQL 语句中创建变量,通常用于模糊查询的字符串拼接。

7.1 基本用法

xml

<select id="getUserByUsername" resultType="User">
    <bind name="pattern" value="'%' + username + '%'"/>
    SELECT * FROM user
    WHERE username LIKE #{pattern}
</select>

优势

  • 避免数据库方言差异(MySQL 用CONCAT,Oracle 用||
  • 提高代码可移植性
  • 可在多个地方复用变量

八、sql/include 标签:SQL 片段复用

sql标签用于定义可复用的 SQL 片段,include标签用于引用这些片段。

8.1 基本用法

xml

<!-- 定义SQL片段 -->
<sql id="userColumns">
    id, username, password, age, email
</sql>

<!-- 引用SQL片段 -->
<select id="getUserById" resultType="User">
    SELECT <include refid="userColumns"/> FROM user
    WHERE id = #{id}
</select>

<select id="getAllUsers" resultType="User">
    SELECT <include refid="userColumns"/> FROM user
</select>

8.2 带参数的 SQL 片段

xml

<sql id="whereCondition">
    <where>
        <if test="param != null">
            AND ${column} = #{param}
        </if>
    </where>
</sql>

<select id="queryByColumn" resultType="User">
    SELECT * FROM user
    <include refid="whereCondition">
        <property name="column" value="username"/>
    </include>
</select>

九、动态 SQL 实战案例

9.1 复杂查询场景

实现一个支持分页、排序、多条件的用户查询功能:

Mapper 接口

java

运行

List<User> queryUserAdvanced(
    @Param("username") String username,
    @Param("minAge") Integer minAge,
    @Param("maxAge") Integer maxAge,
    @Param("email") String email,
    @Param("orderColumn") String orderColumn,
    @Param("orderType") String orderType,
    @Param("start") Integer start,
    @Param("pageSize") Integer pageSize
);

映射文件

xml

<select id="queryUserAdvanced" resultType="User">
    SELECT id, username, age, email FROM user
    <where>
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="minAge != null">
            AND age >= #{minAge}
        </if>
        <if test="maxAge != null">
            AND age <= #{maxAge}
        </if>
        <if test="email != null and email != ''">
            AND email LIKE CONCAT('%', #{email}, '%')
        </if>
    </where>
    
    <if test="orderColumn != null and orderColumn != ''">
        ORDER BY ${orderColumn}
        <if test="orderType != null and orderType != ''">
            ${orderType}
        </if>
        <if test="orderType == null or orderType == ''">
            ASC
        </if>
    </if>
    
    <if test="start != null and pageSize != null">
        LIMIT #{start}, #{pageSize}
    </if>
</select>

9.2 动态更新非空字段

xml

<update id="updateUserDynamic">
    UPDATE user
    <trim prefix="SET" suffixOverrides=",">
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="password != null and password != ''">
            password = #{password},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
        <if test="email != null and email != ''">
            email = #{email},
        </if>
        <if test="updateTime != null">
            update_time = #{updateTime}
        </if>
    </trim>
    WHERE id = #{id}
</update>

十、动态 SQL 最佳实践

  1. 保持 SQL 片段简洁

    • 每个动态 SQL 片段专注于单一功能
    • 避免嵌套过深(建议不超过 3 层)
  2. 合理使用 SQL 片段复用

    • 将常用字段列表、条件判断封装为 SQL 片段
    • 避免过度复用导致的维护困难
  3. 注意 SQL 注入风险

    • 动态排序、表名使用${}时必须进行白名单校验
    • 示例:

    java

    运行

    // 排序字段白名单校验
    if (!Arrays.asList("id", "username", "age").contains(orderColumn)) {
        orderColumn = "id"; // 默认值
    }
    // 排序方向校验
    if (!Arrays.asList("ASC", "DESC").contains(orderType)) {
        orderType = "ASC";
    }
    
  4. 性能考虑

    • 避免不必要的条件判断
    • 复杂动态 SQL 可考虑拆分为多个方法
  5. 测试覆盖

    • 为动态 SQL 的各种分支场景编写测试用例
    • 确保所有条件组合都能正确生成 SQL

十一、常见问题与解决方案

  1. 动态 SQL 生成错误的 SQL

    • 启用日志打印生成的 SQL(logImpl=LOG4J
    • 检查条件判断逻辑和标签嵌套
  2. foreach 标签 collection 属性错误

    • 单个集合参数且无@Param时,collection 值为list(List)或array(数组)
    • 使用@Param注解明确指定集合名称
  3. set 标签导致的 SQL 语法错误

    • 确保至少有一个字段会被更新
    • 在 Service 层进行参数校验
  4. 模糊查询在不同数据库的兼容性

    • 使用bind标签统一处理模糊查询
    • 避免直接使用数据库特定函数

总结

MyBatis 的动态 SQL 是处理复杂查询场景的强大工具,通过ifwhereforeach等标签,可以优雅地生成各种条件的 SQL 语句,避免手动拼接的繁琐和错误。本文详细介绍了动态 SQL 的常用标签和实战案例,掌握这些知识能够帮助你应对各种复杂的查询需求。在实际开发中,应合理运用动态 SQL,同时注意 SQL 安全和性能问题,编写高效、可维护的数据库访问代码。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值