水进的Java开发岗,试用期求生之Mybatis中动态sql的使用

Mybatis动态SQL精讲:让你的SQL语句活起来!

你好,各位伙伴们!我是小猴头,专注后端开发,RPA,AI前沿技术分享。今天,我们要深入探讨Mybatis中一个非常强大且实用的特性——动态SQL。

在实际项目开发中,我们经常会遇到需要根据不同的查询条件构建不同的SQL语句的场景。如果手动拼接SQL字符串,不仅代码冗余、可读性差,而且容易出错,甚至存在SQL注入的风险。Mybatis的动态SQL正是为了解决这些问题而生。它提供了一套简洁、灵活的标签,让我们能够优雅地构建动态变化的SQL语句。

这篇文章将带你全面了解Mybatis动态SQL,掌握其核心标签的使用方法,并结合实际示例进行讲解。

一、 什么是Mybatis动态SQL?

简单来说,动态SQL是一种在运行时根据特定条件动态生成SQL语句的技术。 Mybatis的动态SQL元素是基于XML的,它利用强大的OGNL(Object-Graph Navigation Language)表达式来判断条件,并根据判断结果来构建最终执行的SQL。

Mybatis的动态SQL主要解决了以下痛点:

  1. 手动拼接SQL的繁琐和易错: 避免了Java代码中大量的if/else和字符串拼接操作。
  2. 条件复杂时的可读性差: XML配置方式使得SQL结构更清晰。
  3. WHERE/SET子句的处理: 自动处理多余的ANDOR或逗号,避免手动判断和删除。

二、 Mybatis动态SQL核心标签详解

Mybatis提供了多种动态SQL标签,它们各司其职,共同构成了强大的动态SQL能力。下面我们一一介绍这些常用标签及其用法。

1. <if> 标签:简单的条件判断

<if> 标签是最基础的动态SQL标签,用于根据条件包含或排除SQL片段。

  • 使用语法:

    <if test="判断条件">
        SQL语句片段
    </if>
    
    • test 属性:用于编写判断表达式,支持OGNL语法。例如:username != null and username != '', age > 18, list.size() > 0, isDelete == true 等。
    • test 表达式中访问对象属性可以直接使用属性名,访问Map中的值使用key,访问方法参数如果是单个基本类型或字符串可以直接使用参数名,如果是多个参数,需要使用 @Param 注解指定名称或将参数封装到Map/DTO中,通过名称访问。
  • 示例:

    <select id="findUsersByCriteria" 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="gender != null and gender != ''">
            AND gender = #{gender}
        </if>
    </select>
    
    • 注意点:

      • 对于字符串判空,通常使用 != null and != ''
      • 对于数值类型,只需要判断 != null 即可,因为数值类型的默认值(如int的0)不会被判断为空字符串。避免在数值类型的非空判断中加入 != ''
      • List判空和大小判断:list != null and list.size() > 0
      • 布尔类型直接使用属性名即可,如 isDelete
    • 特殊说明: 在某些旧版本的Mybatis或特定配置下,字符串的equals比较可能需要使用 eq 操作符或 .toString() 方法,但通常情况下,直接使用 ==!= 进行字符串 比较是Mybatis/OGNL推荐且正常工作的(它比较的是字符串内容,而非对象引用)。例如:<if test="equipAttribute == '易碎品'"> 是标准的写法。

2. <where> 标签:智能处理WHERE子句

<where> 标签非常适合用于 SELECT 语句的 WHERE 子句。它会自动处理以下情况:

  • 如果 <where> 标签内的所有条件都不满足,则不会生成 WHERE 关键字。

  • 如果 <where> 标签内的第一个有效条件是以 ANDOR 开头,<where> 标签会自动移除开头的 ANDOR

  • 使用语法:

    <where>
        <if test="...">
            AND condition1
        </if>
        <if test="...">
            OR condition2
        </if>
        </where>
    
  • 示例:

    <select id="findStrategies" resultType="StrategyDto">
        SELECT strategy_name, service_id, status
        FROM t_strategy
        <where>
            <if test="dto.strategyName != null and dto.strategyName != ''">
                AND strategy_name LIKE CONCAT('%', #{dto.strategyName}, '%')
            </if>
            <if test="dto.serviceId != null">
                AND service_id = #{dto.serviceId}
            </if>
            <if test="dto.status != null">
                AND status = #{dto.status}
            </if>
        </where>
    </select>
    
    • 对比: 使用 <where> 比手动在 WHERE 子句后加上 1=1 的方式更优雅。

3. <set> 标签:智能处理SET子句

<set> 标签主要用于 UPDATE 语句的 SET 子句。它会自动处理以下情况:

  • 如果 <set> 标签内的所有条件都不满足,则不会生成 SET 关键字(尽管这通常不是我们期望的更新行为)。

  • 如果 <set> 标签内的最后一个有效赋值语句以逗号 , 结尾,<set> 标签会自动移除该多余的逗号。

  • 使用语法:

    <set>
        <if test="...">
            column1 = value1,
        </if>
        <if test="...">
            column2 = value2,
        </if>
        </set>
    
  • 示例:

    <update id="updateAlarmInfo">
        UPDATE t_alarm_info
        <set>
            <if test="item.alarmTitle != null and item.alarmTitle != ''" >
                alarm_title = #{item.alarmTitle,jdbcType=VARCHAR},
            </if>
            <if test="item.alarmLevel != null" > alarm_level = #{item.alarmLevel,jdbcType=INTEGER},
            </if>
            <if test="item.alarmRule != null and item.alarmRule != ''" >
                alarm_rule = #{item.alarmRule,jdbcType=VARCHAR},
            </if>
            <if test="item.lastTime != null" >
                last_time = #{item.lastTime,jdbcType=TIMESTAMP},
            </if>
            <if test="item.recoveryTime != null " >
                recovery_time = #{item.recoveryTime,jdbcType=TIMESTAMP},
            </if>
            <if test="item.status != null" >
                status = #{item.status,jdbcType=INTEGER},
            </if>
        </set>
        WHERE alarm_id = #{item.alarmId}
    </update>
    

4. <choose>, <when>, <otherwise> 标签:多分支选择

<choose>, <when>, <otherwise> 标签组合类似于 Java 中的 if/else if/else 结构。它用于在多个条件中选择其中一个执行。

  • 使用语法:

    <choose>
        <when test="判断条件1">
            SQL语句片段1
        </when>
        <when test="判断条件2">
            SQL语句片段2
        </when>
        <otherwise>
            SQL语句片段N
        </otherwise>
    </choose>
    
    • <choose>:父标签,包裹所有条件分支。
    • <when>:条件分支,至少有一个。Mybatis会从上到下依次判断 test 属性,一旦有 when 条件满足,就会执行其内容,并跳过后续所有的 whenotherwise
    • <otherwise>:默认分支,当所有 when 条件都不满足时执行(可选)。
  • 示例:

    <select id="queryByUserNameOrAddress" resultType="User">
        SELECT * FROM user
        WHERE sex='男'
        <choose>
            <when test="userName != null and userName.trim() != ''">
                AND username LIKE CONCAT('%', #{userName}, '%')
            </when>
            <when test="address != null and address.trim() != ''">
                AND address = #{address}
            </when>
            <otherwise>
                AND username='孙悟空'
            </otherwise>
        </choose>
    </select>
    

5. <foreach> 标签:遍历集合或数组

<foreach> 标签用于遍历集合(List、Set等)或数组,常用于构建 IN 条件或批量插入/更新语句。

  • 使用语法:

    <foreach collection="集合名或者数组名"
             item="元素变量名"
             index="索引变量名"
             open="循环开始前插入的字符串"
             close="循环结束后插入的字符串"
             separator="循环体之间插入的字符串">
        #{元素变量名} 或 ${元素变量名}
    </foreach>
    
    • collection: 必填。指定要遍历的集合或数组参数名。如果方法参数使用了 @Param("myList"),则此处写 "myList";如果参数是List类型且未加 @Param,默认是 "list";如果是数组默认是 "array";如果是Map,可以是 "map"
    • item: 必填。循环中当前元素的变量名。
    • index: 可选。循环中当前元素索引的变量名(用于List/数组)或Map的key(用于Map)。
    • open: 可选。整个 foreach 块开始前添加的字符串(如 ( 用于 IN 语句)。
    • close: 可选。整个 foreach 块结束后添加的字符串(如 ) 用于 IN 语句)。
    • separator: 可选。每个循环元素之间添加的字符串(如 , 用于 IN 语句或插入语句)。
  • 示例:

    1. 构建 IN 条件:
    <select id="getAlarmsByIds" resultType="Alarm">
        SELECT * FROM alarm
        WHERE id IN
        <foreach collection="alarmIds" item="id" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </select>
    

    对应 Java 方法签名可能为 List<Alarm> getAlarmsByIds(@Param("alarmIds") List<String> alarmIds);

    1. 批量插入:
    <insert id="insertBatch">
        INSERT INTO t_alarm_info (alarm_id, instance_id, instance_name, product_name, alarm_title, alarm_level, alarm_rule, occur_time, last_time, recovery_time, status, cr_time, up_time)
        VALUES
        <foreach collection="dtoList" item="item" separator=",">
            (
             #{item.alarmId,jdbcType=VARCHAR},
             #{item.instanceId,jdbcType=VARCHAR},
             #{item.instanceName,jdbcType=VARCHAR},
             #{item.instanceGroup,jdbcType=VARCHAR},
             #{item.productName},
             #{item.alarmTitle,jdbcType=VARCHAR},
             #{item.alarmLevel,jdbcType=INTEGER},
             #{item.alarmRule,jdbcType=VARCHAR},
             #{item.occurTime,jdbcType=TIMESTAMP},
             #{item.lastTime,jdbcType=TIMESTAMP},
             #{item.recoveryTime,jdbcType=TIMESTAMP},
             #{item.status,jdbcType=INTEGER},
             now(),
             now()
            )
        </foreach>
    </insert>
    

    对应 Java 方法签名可能为 void insertBatch(@Param("dtoList") List<ThirdCloudAlarm> addList);

    1. 批量更新(使用分号分隔,可能需要数据库驱动支持多语句执行):
    <update id="updateBatch">
        <foreach collection="dtoList" item="item" separator=";" >
            UPDATE t_alarm_info
            <set >
                <if test="item.alarmTitle != null and item.alarmTitle != ''" >
                    alarm_title = #{item.alarmTitle,jdbcType=VARCHAR},
                </if>
                <if test="item.alarmLevel != null" >
                   alarm_level = #{item.alarmLevel,jdbcType=INTEGER},
                 </if>
                 <if test="item.alarmRule != null and item.alarmRule != ''" >
                   alarm_rule = #{item.alarmRule,jdbcType=VARCHAR},
                 </if>
                 <if test="item.lastTime != null" >
                   last_time = #{item.lastTime,jdbcType=TIMESTAMP},
                 </if>
                 <if test="item.recoveryTime != null " >
                   recovery_time = #{item.recoveryTime,jdbcType=TIMESTAMP},
                 </if>
                <if test="item.status != null" >
                    status = #{item.status,jdbcType=INTEGER},
                </if>
            </set>
            WHERE alarm_id = #{item.alarmId}
        </foreach>
    </update>
    

    对应 Java 方法签名可能为 void updateBatch(@Param("dtoList") List<ThirdCloudAlarmDto> updateList);

    • 重要提示: 使用 separator=";" 进行批量更新需要数据库连接URL中开启多语句支持,例如MySQL连接字符串可能需要加上 allowMultiQueries=true。这种方式不如 ExecutorType.BATCH 高效和推荐,但作为动态SQL的应用场景之一,可以了解。

6. <trim> 标签:通用的SQL片段修饰

<trim> 标签是一个非常灵活的标签,可以用来修饰 SQL 片段,实现类似于 <where><set> 的功能,或者处理更复杂的字符串拼接需求。

  • 使用语法:

    <trim prefix="前缀"
          prefixOverrides="需要去除的前缀"
          suffix="后缀"
          suffixOverrides="需要去除的后缀">
        SQL语句片段
    </trim>
    
    • prefix: 可选。在 <trim> 标签体内容前面添加的字符串。
    • prefixOverrides: 可选。去除 <trim> 标签体内容开头的指定字符串(多个用 | 分隔)。
    • suffix: 可选。在 <trim> 标签体内容后面添加的字符串。
    • suffixOverrides: 可选。去除 <trim> 标签体内容结尾的指定字符串(多个用 | 分隔)。
  • 示例:

    1. 模拟 <where> 的功能:
    <trim prefix="WHERE" prefixOverrides="AND | OR">
        <if test="dto.strategyName != null and dto.strategyName != ''">
            AND strategy_name LIKE CONCAT('%', #{dto.strategyName}, '%')
        </if>
        <if test="dto.serviceId != null">
            AND service_id = #{dto.serviceId}
        </if>
        <if test="dto.status != null">
            AND status = #{dto.status}
        </if>
    </trim>
    

    这里 prefix="WHERE" 会在生成的内容前加 WHEREprefixOverrides="AND | OR" 会去除内容开头的 ANDOR

    1. 模拟 <set> 的功能:
    <trim prefix="SET" suffixOverrides=",">
        <if test="item.alarmTitle != null and item.alarmTitle != ''" >
            alarm_title = #{item.alarmTitle,jdbcType=VARCHAR},
        </if>
        <if test="item.status != null" >
            status = #{item.status,jdbcType=INTEGER},
        </if>
    </trim>
    

    这里 prefix="SET" 会在生成的内容前加 SETsuffixOverrides="," 会去除内容结尾的逗号。

    1. 用于 INSERT 语句构建:
    <insert id="insertSelective" parameterType="com.zcloud.domain.warehousemanage.dto.WmEquipInfoDto">
        insert into t_wm_equip_info
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="equipCode != null">
                equip_code,
            </if>
            <if test="equipAttribute != null">
                equip_attribute,
            </if>
            <if test="equipName != null">
                equip_name,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="equipCode != null">
                #{equipCode,jdbcType=VARCHAR},
            </if>
            <if test="equipAttribute != null">
                #{equipAttribute,jdbcType=VARCHAR},
            </if>
            <if test="equipName != null">
                #{equipName,jdbcType=VARCHAR},
            </if>
        </trim>
    </insert>
    

    这个示例利用两个 <trim> 分别构建了 INSERT 语句的字段列表和值列表,同时通过 suffixOverrides="," 优雅地处理了末尾的逗号。

7. <sql><include> 标签:SQL片段复用

虽然在原始内容中没有提及,但 <sql><include> 标签是动态SQL中非常重要的一部分,用于定义可重用的SQL片段。这极大地提高了SQL映射文件的可维护性。

  • 使用语法:

    <sql id="sql片段ID">
        可重用的SQL语句片段
    </sql>
    
    <include refid="sql片段ID"/>
    
  • 示例:

    <sql id="baseColumns">
        id, username, age, gender, address
    </sql>
    
    <select id="selectAllUsers" resultType="User">
        SELECT
        <include refid="baseColumns"/>
        FROM user
    </select>
    
    <select id="findUserById" resultType="User">
        SELECT
        <include refid="baseColumns"/>
        FROM user
        WHERE id = #{id}
    </select>
    

三、动态SQL的使用技巧与注意事项

  • OGNL表达式: 熟悉OGNL语法是编写动态SQL的关键。掌握如何访问属性、调用方法、进行比较等。
  • #{} vs ${}: 在动态SQL中,尽量使用 #{} 来引用参数,它会使用预编译语句,有效防止SQL注入。只有在需要动态改变SQL结构(如动态表名、动态排序字段)时才使用 ${},但要注意潜在的SQL注入风险。在 <foreach> 标签中构建 IN 条件时,#{item} 是安全的。
  • 提高可读性: 适当使用缩进和换行,让复杂的动态SQL块更易读。
  • 结合使用: 多个动态SQL标签可以嵌套和组合使用,以构建更复杂的逻辑。
  • 测试: 对于复杂的动态SQL,务必编写充分的单元测试或集成测试,验证在各种条件下的SQL生成是否正确。

四、总结

Mybatis动态SQL是其核心特性之一,它通过一系列强大的标签 (<if>, <where>, <set>, <choose>, <foreach>, <trim>, <sql>, <include>),将复杂的条件判断和循环逻辑从Java代码中解放出来,转移到XML映射文件中,使得SQL语句更加灵活、可维护、可读性高,并有效避免了手动拼接SQL带来的问题。

熟练掌握Mybatis动态SQL,将让你在处理复杂业务逻辑时更加得心应手。希望本文能帮助你深入理解并更好地应用Mybatis动态SQL!

如果你在实践中遇到任何问题,或者有其他动态SQL的技巧分享,欢迎在评论区留言交流!

一回生,二回熟,三次四次好朋友,我是小猴头,下期见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值