MyBatis 动态查询语句详解:让 SQL 更灵活可控
在日常的数据库操作中,我们经常会遇到需要根据不同条件拼接 SQL 语句的场景。比如查询用户时,可能需要根据姓名、年龄、性别等多个条件进行筛选,而这些条件往往是动态变化的 —— 有时需要按姓名查,有时需要按年龄查,有时又需要组合多个条件。如果手动拼接 SQL,不仅容易出错,还会让代码变得臃肿难维护。MyBatis 的动态查询语句正是为解决这一问题而生,它能根据参数的不同自动拼接 SQL 片段,让 SQL 编写更加灵活高效。
一、动态查询的核心价值
动态查询是 MyBatis 的核心特性之一,它允许我们在 XML 映射文件或注解中,通过一系列标签控制 SQL 片段的拼接逻辑。其核心价值体现在三个方面:
-
减少冗余代码:无需为不同条件组合编写大量重复的 SQL 语句。
-
避免 SQL 注入风险:MyBatis 的动态标签会自动处理参数拼接,比手动字符串拼接更安全。
-
提升代码可读性:将条件判断逻辑与 SQL 语句分离,让业务逻辑更清晰。
举个简单的例子:查询用户列表时,可能需要根据姓名(name)和年龄(age)筛选。如果没有动态查询,我们可能需要编写 3 条 SQL(只查 name、只查 age、同时查 name 和 age);而有了动态查询,1 条 SQL 就能搞定所有场景。
二、常用动态查询标签详解
MyBatis 提供了一套完整的动态 SQL 标签,涵盖了大多数条件判断场景。下面介绍最常用的几个标签及其用法。
1. <if>
:条件判断的基础
<if>
标签是动态查询中最常用的标签,用于根据参数值决定是否拼接某个 SQL 片段。其语法如下:
<if test="条件表达式">SQL片段
</if>
示例:根据姓名和年龄查询用户
<select id="selectUser" resultType="User">SELECT * FROM userWHERE 1=1<if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if><if test="age != null">AND age = #{age}</if>
</select>
这里的WHERE 1=1
是为了避免当所有<if>
条件都不满足时,SQL 出现WHERE
后无内容的语法错误。当name
不为空时,会拼接AND name LIKE ...
;当age
不为空时,会拼接AND age = ...
。
2. <choose>
+<when>
+<otherwise>
:多条件分支选择
当需要实现 “多条件选其一” 的逻辑时,可以使用<choose>
标签(类似 Java 中的switch
),配合<when>
(类似case
)和<otherwise>
(类似default
)标签使用。
示例:优先按姓名查询,若姓名为空则按年龄查询,否则查询所有
<select id="selectUserByCondition" resultType="User">SELECT * FROM user<where><choose><when test="name != null and name != ''">name LIKE CONCAT('%', #{name}, '%')</when><when test="age != null">age = #{age}</when><otherwise>1=1 <!-- 当所有条件都不满足时,查询所有数据 --></otherwise></choose></where>
</select>
<where>
标签会自动处理 SQL 片段开头的AND
或OR
,避免语法错误。比如当第一个<when>
条件满足时,拼接的name LIKE ...
前不会有多余的AND
。
3. <trim>
:自定义 SQL 片段拼接规则
<trim>
标签可以通过属性自定义 SQL 片段的前缀、后缀,以及需要去掉的前缀或后缀字符,比<where>
更灵活。常用属性:
-
prefix
:给拼接的 SQL 片段添加前缀 -
suffix
:给拼接的 SQL 片段添加后缀 -
prefixOverrides
:去掉片段开头的指定字符 -
suffixOverrides
:去掉片段结尾的指定字符
示例:用<trim>
实现<where>
的功能
<select id="selectUser" resultType="User">SELECT * FROM user<trim prefix="WHERE" prefixOverrides="AND | OR"><if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if><if test="age != null">AND age = #{age}</if></trim>
</select>
当<trim>
内部有内容时,会添加WHERE
前缀,并去掉开头的AND
或OR
,效果与<where>
一致。
4. <foreach>
:遍历集合参数
当需要处理批量操作(如IN
查询、批量插入)时,<foreach>
标签非常有用。它可以遍历数组或集合,将元素拼接成 SQL 片段。常用属性:
-
collection
:指定要遍历的集合参数名(如list
、array
或@Param
定义的名称) -
item
:遍历过程中每个元素的别名 -
index
:遍历索引(可选) -
open
:片段开头的字符 -
close
:片段结尾的字符 -
separator
:元素之间的分隔符
示例 1:批量查询(IN 语句)
<select id="selectUserByIds" resultType="User">SELECT * FROM userWHERE id IN<foreach collection="ids" item="id" open="(" close=")" separator=",">#{id}</foreach>
</select>
当ids
为[1,2,3]
时,会拼接成WHERE id IN (1,2,3)
。
示例 2:批量插入
<insert id="batchInsertUser">INSERT INTO user (name, age) VALUES<foreach collection="list" item="user" separator=",">(#{user.name}, #{user.age})</foreach>
</insert>
当list
包含 3 个 User 对象时,会拼接成INSERT INTO user (name, age) VALUES (?,?), (?,?), (?,?)
。
5. <set>
:更新语句的动态处理
在更新操作中,<set>
标签用于动态拼接SET
子句,会自动去掉多余的逗号。
示例:动态更新用户信息
<update id="updateUser">UPDATE user<set><if test="name != null and name != ''">name = #{name},</if><if test="age != null">age = #{age},</if></set>WHERE id = #{id}
</update>
当name
和age
都不为空时,会拼接成UPDATE user SET name = ?, age = ? WHERE id = ?
,自动去掉age = ?
后的逗号。
三、实战案例:综合运用动态标签
下面通过一个复杂案例,展示如何综合运用上述标签实现多条件组合查询。
需求:查询商品列表,支持按分类(category)、价格范围(minPrice、maxPrice)、是否库存(hasStock)筛选,且支持排序(sortField、sortType)。
Mapper 接口:
List<Product> selectProducts(@Param("category") String category,@Param("minPrice") BigDecimal minPrice,@Param("maxPrice") BigDecimal maxPrice,@Param("hasStock") Boolean hasStock,@Param("sortField") String sortField,@Param("sortType") String sortType
);
XML 映射文件:
<select id="selectProducts" resultType="Product">SELECT * FROM product<trim prefix="WHERE" prefixOverrides="AND | OR"><if test="category != null and category != ''">AND category = #{category}</if><if test="minPrice != null">AND price >= #{minPrice}</if><if test="maxPrice != null">AND price <= #{maxPrice}</if><if test="hasStock != null">AND stock > 0</if></trim><if test="sortField != null and sortField != '' and sortType != null">ORDER BY ${sortField} ${sortType} <!-- 排序字段用${},注意SQL注入风险 --></if>
</select>
说明:
-
用
<trim>
处理查询条件,避免多余的AND
-
用
<if>
判断每个筛选条件是否生效 -
排序部分通过
<if>
控制是否拼接ORDER BY
,注意排序字段使用${}
(因为#{}
会加引号,导致语法错误),但需确保sortField
和sortType
是可信参数,避免 SQL 注入。
四、注意事项与最佳实践
- 参数判断的细节:
-
字符串判断:
test="name != null and name != ''"
(先判断非空,再判断非空字符串) -
数字判断:
test="age != null"
(无需判断空字符串) -
布尔值判断:
test="hasStock != null and hasStock"
(直接用参数名判断 true/false)
- SQL 注入风险:
-
动态 SQL 中,
#{}
会自动加引号,适合参数值;${}
直接拼接 SQL,适合表名、字段名等,但需严格校验参数,避免注入。 -
排序、分组等场景需用
${}
时,建议限制可选值(如sortField
只能是id
、price
等已知字段)。
- 代码可读性:
-
复杂动态 SQL 建议拆分到 XML 文件,而非注解中
-
给参数添加
@Param
注解,避免使用arg0
、arg1
-
合理使用换行和缩进,保持 SQL 结构清晰
- 性能优化:
-
避免过度复杂的动态 SQL,否则可能影响数据库优化器生成执行计划
-
频繁执行的动态 SQL,建议通过 MyBatis 的二级缓存缓存结果
五、总结
MyBatis 的动态查询语句通过<if>
、<choose>
、<foreach>
等标签,完美解决了 SQL 条件动态拼接的问题,让开发者能更专注于业务逻辑而非 SQL 语法细节。掌握这些标签的用法,不仅能减少代码量,还能提升系统的灵活性和可维护性。
在实际开发中,建议结合具体业务场景选择合适的标签,同时注意参数校验和 SQL 注入风险,让动态 SQL 成为提升开发效率的利器而非隐患。