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
关键字 - 自动去除条件片段开头的
AND
或OR
- 当所有条件都不成立时,不添加
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 userWHERE 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 userSET 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 userWHERE 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 userWHERE 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 最佳实践
保持 SQL 片段简洁:
- 每个动态 SQL 片段专注于单一功能
- 避免嵌套过深(建议不超过 3 层)
合理使用 SQL 片段复用:
- 将常用字段列表、条件判断封装为 SQL 片段
- 避免过度复用导致的维护困难
注意 SQL 注入风险:
- 动态排序、表名使用
${}
时必须进行白名单校验 - 示例:
java
运行
// 排序字段白名单校验 if (!Arrays.asList("id", "username", "age").contains(orderColumn)) {orderColumn = "id"; // 默认值 } // 排序方向校验 if (!Arrays.asList("ASC", "DESC").contains(orderType)) {orderType = "ASC"; }
- 动态排序、表名使用
性能考虑:
- 避免不必要的条件判断
- 复杂动态 SQL 可考虑拆分为多个方法
测试覆盖:
- 为动态 SQL 的各种分支场景编写测试用例
- 确保所有条件组合都能正确生成 SQL
十一、常见问题与解决方案
动态 SQL 生成错误的 SQL:
- 启用日志打印生成的 SQL(
logImpl=LOG4J
) - 检查条件判断逻辑和标签嵌套
- 启用日志打印生成的 SQL(
foreach 标签 collection 属性错误:
- 单个集合参数且无
@Param
时,collection 值为list
(List)或array
(数组) - 使用
@Param
注解明确指定集合名称
- 单个集合参数且无
set 标签导致的 SQL 语法错误:
- 确保至少有一个字段会被更新
- 在 Service 层进行参数校验
模糊查询在不同数据库的兼容性:
- 使用
bind
标签统一处理模糊查询 - 避免直接使用数据库特定函数
- 使用
总结
MyBatis 的动态 SQL 是处理复杂查询场景的强大工具,通过if
、where
、foreach
等标签,可以优雅地生成各种条件的 SQL 语句,避免手动拼接的繁琐和错误。本文详细介绍了动态 SQL 的常用标签和实战案例,掌握这些知识能够帮助你应对各种复杂的查询需求。在实际开发中,应合理运用动态 SQL,同时注意 SQL 安全和性能问题,编写高效、可维护的数据库访问代码。