一、动态 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 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:条件分支,类似caseotherwise:默认分支,类似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 最佳实践
-
保持 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 安全和性能问题,编写高效、可维护的数据库访问代码。
1582

被折叠的 条评论
为什么被折叠?



