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主要解决了以下痛点:
- 手动拼接SQL的繁琐和易错: 避免了Java代码中大量的
if/else
和字符串拼接操作。 - 条件复杂时的可读性差: XML配置方式使得SQL结构更清晰。
- WHERE/SET子句的处理: 自动处理多余的
AND
、OR
或逗号,避免手动判断和删除。
二、 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>
标签内的第一个有效条件是以AND
或OR
开头,<where>
标签会自动移除开头的AND
或OR
。 -
使用语法:
<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
条件满足,就会执行其内容,并跳过后续所有的when
和otherwise
。<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 语句或插入语句)。
-
示例:
- 构建
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);
- 批量插入:
<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);
- 批量更新(使用分号分隔,可能需要数据库驱动支持多语句执行):
<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>
标签体内容结尾的指定字符串(多个用|
分隔)。
-
示例:
- 模拟
<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"
会在生成的内容前加WHERE
,prefixOverrides="AND | OR"
会去除内容开头的AND
或OR
。- 模拟
<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"
会在生成的内容前加SET
,suffixOverrides=","
会去除内容结尾的逗号。- 用于 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的技巧分享,欢迎在评论区留言交流!