在日常开发中,我们经常会遇到需要根据不同条件动态生成 SQL 的场景,比如多条件查询、选择性更新、批量操作等。MyBatis 提供的动态 SQL 标签正是解决这类问题的利器,它能让我们的 SQL 语句更加灵活、简洁,同时避免手动拼接 SQL 带来的安全隐患(如 SQL 注入)。本文将通过具体案例,详细讲解如何使用 MyBatis 动态标签实现完整的增删改查操作。
一、MyBatis 动态标签核心语法
MyBatis 动态 SQL 基于 XML 标签实现,常用标签包括:
标签 | 作用 | 适用场景 |
---|---|---|
<if> | 条件判断,满足条件则拼接 SQL 片段 | 多条件查询、选择性更新 |
<where> | 自动处理条件前缀的 AND/OR 关键字 | 替代 WHERE 1=1 的冗余写法 |
<set> | 自动处理更新语句的逗号分隔符 | 更新操作时排除空值字段 |
<choose> | 多条件分支判断(类似 Java 的 switch ) | 优先匹配条件的场景 |
<foreach> | 遍历集合或数组 | 批量查询(IN )、批量插入 |
<trim> | 自定义字符串截取规则 | 灵活处理前缀 / 后缀 |
这些标签可以组合使用,实现复杂的动态 SQL 逻辑。接下来我们通过实战案例学习它们的具体用法。
二、环境准备
在开始之前,我们需要准备基础环境:
- 数据库表结构(以用户表
user
为例):
sql
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`age` int DEFAULT NULL COMMENT '年龄',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`status` tinyint DEFAULT 1 COMMENT '状态(1-正常,0-禁用)',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 实体类
User
:
public class User {
private Integer id;
private String username;
private Integer age;
private String email;
private Integer status;
private LocalDateTime createTime;
// 省略 getter、setter 和构造方法
}
- Mapper 接口
UserMapper
:
public interface UserMapper {
// 增删改查方法声明(后文逐步实现)
}
三、动态 SQL 实现增删改查实战
1. 动态查询(多条件筛选)
需求:实现一个用户列表查询接口,支持根据用户名模糊搜索、年龄筛选、状态筛选,条件为空时不参与过滤。
Mapper 接口方法:
List<User> selectByCondition(User user);
XML 映射文件:
xml
<select id="selectByCondition" resultType="User">
SELECT id, username, age, email, status, create_time
FROM user
<where>
<!-- 用户名模糊查询:不为 null 且不为空字符串时生效 -->
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<!-- 年龄筛选:不为 null 时生效 -->
<if test="age != null">
AND age = #{age}
</if>
<!-- 状态筛选:不为 null 时生效 -->
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY create_time DESC
</select>
核心说明:
- 使用
<where>
标签替代传统的WHERE 1=1
,它会自动去除条件中多余的AND/OR
关键字。 <if>
标签的test
属性支持 OGNL 表达式,判断参数是否满足条件。- 模糊查询使用
CONCAT('%', #{username}, '%')
避免 SQL 注入(不推荐直接拼接字符串)。
2. 动态更新(选择性更新字段)
需求:实现用户信息更新接口,只更新传入了非空值的字段,未传入的字段保持不变。
Mapper 接口方法:
int updateSelective(User user);
XML 映射文件:
xml
<update id="updateSelective">
UPDATE user
<set>
<!-- 用户名:不为 null 且不为空时更新 -->
<if test="username != null and username != ''">
username = #{username},
</if>
<!-- 年龄:不为 null 时更新 -->
<if test="age != null">
age = #{age},
</if>
<!-- 邮箱:不为 null 且不为空时更新 -->
<if test="email != null and email != ''">
email = #{email},
</if>
<!-- 状态:不为 null 时更新 -->
<if test="status != null">
status = #{status}
</if>
</set>
WHERE id = #{id}
</update>
核心说明:
<set>
标签会自动处理字段后的逗号,避免因最后一个字段带逗号导致的 SQL 语法错误。- 必须传入
id
作为更新条件,否则会更新全表数据(非常危险!)。
3. 动态插入(选择性插入字段)
需求:实现用户新增接口,支持只传入部分必填字段,其他可选字段使用数据库默认值。
Mapper 接口方法:
int insertSelective(User user);
XML 映射文件:
xml
<insert id="insertSelective" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username != null and username != ''">
username,
</if>
<if test="age != null">
age,
</if>
<if test="email != null and email != ''">
email,
</if>
<if test="status != null">
status,
</if>
<if test="createTime != null">
create_time,
</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="username != null and username != ''">
#{username},
</if>
<if test="age != null">
#{age},
</if>
<if test="email != null and email != ''">
#{email},
</if>
<if test="status != null">
#{status},
</if>
<if test="createTime != null">
#{createTime},
</if>
</trim>
</insert>
核心说明:
- 使用
<trim>
标签自定义拼接规则:prefix
定义前缀,suffix
定义后缀,suffixOverrides=","
去除末尾多余的逗号。 useGeneratedKeys="true" keyProperty="id"
用于获取数据库自增主键,并赋值到实体类的id
属性。
4. 批量删除(根据 ID 列表)
需求:实现批量删除用户功能,支持传入多个用户 ID 进行删除。
Mapper 接口方法:
int deleteBatch(@Param("ids") List<Integer> ids);
XML 映射文件:
xml
<delete id="deleteBatch">
DELETE FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
核心说明:
<foreach>
标签用于遍历集合:collection
:指定要遍历的集合参数名(使用@Param
注解定义)。item
:遍历元素的别名。open
和close
:定义遍历结果的前后缀(此处拼接为(1,2,3)
)。separator
:元素之间的分隔符(此处使用逗号)。
5. 分支条件查询(Choose 标签)
需求:实现一个优先级查询接口,优先按用户名查询,若用户名不存在则按邮箱查询,若都不存在则查询状态为正常的用户。
Mapper 接口方法:
List<User> selectByPriority(User user);
XML 映射文件:
xml
<select id="selectByPriority" resultType="User">
SELECT id, username, age, email, status
FROM user
<where>
<choose>
<!-- 优先级 1:用户名不为空时 -->
<when test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</when>
<!-- 优先级 2:邮箱不为空时 -->
<when test="email != null and email != ''">
AND email LIKE CONCAT('%', #{email}, '%')
</when>
<!-- 默认条件:状态为正常 -->
<otherwise>
AND status = 1
</otherwise>
</choose>
</where>
</select>
核心说明:
<choose>
标签类似 Java 的switch
语句,只会执行第一个满足条件的<when>
标签,若都不满足则执行<otherwise>
标签。- 适用于有明确优先级的条件判断场景。
四、动态 SQL 最佳实践
-
避免 SQL 注入:
- 始终使用
#{}
占位符,而非${}
字符串拼接(${}
会直接替换变量,存在注入风险)。 - 模糊查询推荐使用
CONCAT('%', #{param}, '%')
或数据库函数(如 MySQL 的LIKE '%${param}%'
需谨慎使用)。
- 始终使用
-
优化可读性:
- 合理缩进 XML 标签,复杂逻辑可拆分到
<sql>
标签中复用(使用<include>
引用)。 - 示例:
xml
<sql id="baseColumn"> id, username, age, email, status, create_time </sql> <select id="selectById" resultType="User"> SELECT <include refid="baseColumn"/> FROM user WHERE id = #{id} </select>
- 合理缩进 XML 标签,复杂逻辑可拆分到
-
性能考量:
- 动态 SQL 会增加 MyBatis 的解析开销,过于复杂的逻辑建议拆分或通过代码层处理。
- 批量操作(如批量插入)时,注意数据库的 SQL 长度限制,可分批次执行。
-
调试技巧:
- 开启 MyBatis 日志打印 SQL,方便调试动态生成的语句:
yaml
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
五、总结
MyBatis 动态 SQL 标签通过简洁的 XML 语法,让我们能够轻松实现复杂的条件查询、选择性更新、批量操作等功能,极大提升了 SQL 语句的灵活性和可维护性。本文介绍的 <if>
、<where>
、<set>
、<foreach>
、<choose>
等标签是日常开发中最常用的工具,掌握它们的使用技巧能让我们的数据库操作代码更加优雅高效。
在实际项目中,建议结合业务场景合理选择动态标签,避免过度使用导致 SQL 逻辑难以理解。同时,始终注意 SQL 注入风险和性能优化,让动态 SQL 真正成为开发效率的助力而非负担。