set
导学
本节你将掌握 MyBatis 中专门用于处理动态 SET 子句的元素 set。学习目标是:
- 理解
set元素解决的核心痛点(动态 UPDATE 时字段不确定导致的逗号问题) - 掌握
set与if配合实现动态字段更新 - 理解
set的内部实现原理(它是trim的特例) - 能够编写安全、优雅的动态 UPDATE 语句
定义
set 是 MyBatis 动态 SQL 中专门用于智能处理 UPDATE 语句 SET 子句的元素。它会自动完成两项工作:
- 智能插入
SET关键字:只有当内部至少有一个字段需要更新时,才在 SQL 中插入SET - 智能移除多余的逗号:如果最后一个输出的字段后面带有逗号,
set会自动将其移除
在 JDBC 编程中,实现动态更新通常需要复杂的字符串拼接:
String sql = "UPDATE student SET ";
boolean first = true;
if (name != null) {
if (!first) sql += ", ";
sql += "name = '" + name + "'";
first = false;
}
if (major != null) {
if (!first) sql += ", ";
sql += "major = '" + major + "'";
first = false;
}
// ... 还要处理一个字段都没传的情况
这种写法不仅冗长丑陋,而且每个字段都存在 SQL 注入风险。更麻烦的是,如果所有字段都为 null,SQL 会变成 UPDATE student SET 后面没有内容,导致语法错误。MyBatis 的 set 元素将动态更新逻辑声明式地表达在 XML 中,配合 #{ } 预编译占位符,既简洁又安全。
适用位置与核心属性
set 作为容器元素,只能用于 UPDATE 语句内部,嵌套一个或多个 if 等动态 SQL 元素。
| 属性 | 是否必填 | 说明 |
|---|---|---|
| 无 | — | set 元素没有任何属性,它的行为是固定的 |
核心原理
set 智能去逗号流程图
set 的本质:trim 的特例
set 元素在 MyBatis 源码中的实现,本质上就是 trim 元素的一个预设特例:
<!-- set 的底层实现等价于: -->
<trim prefix="SET" suffixOverrides=",">
...
</trim>
这意味着 set 会:
- 在内容前自动加上
SET前缀 - 移除内容末尾多余的逗号
,
完整示例
场景说明
乐途学院的学生管理系统需要提供一个动态字段更新功能:管理员可以只更新学生的姓名,也可以只更新专业,也可以同时更新多个字段。要求使用 set 元素实现,确保无论传入哪些字段,生成的 UPDATE 语句都是语法正确的。
操作前的数据库表结构及初始数据
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
major VARCHAR(20),
score DECIMAL(5,2)
);
INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);
当前表数据:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
完整的映射文件片段
StudentMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.flying.mapper.StudentMapper">
<!-- 动态更新学生信息:set 智能处理 SET 和逗号 -->
<update id="updateStudentDynamic" parameterType="com.flying.entity.Student">
UPDATE student
<set>
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="major != null and major != ''">
major = #{major},
</if>
<if test="score != null">
score = #{score},
</if>
</set>
WHERE id = #{id}
</update>
</mapper>
StudentMapper.java(接口)
package com.flying.mapper;
import com.flying.entity.Student;
public interface StudentMapper {
int updateStudentDynamic(Student student);
}
实际执行结果
情况一:同时更新 name 和 major
测试代码:
Student student = new Student();
student.setId(1);
student.setName("大翔飞");
student.setMajor("人工智能");
mapper.updateStudentDynamic(student);
最终生成的 SQL 语句:
UPDATE student SET name = ?, major = ? WHERE id = ?
参数值: 大翔飞, 人工智能, 1
执行后表数据:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔飞 | 22 | 人工智能 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
分析: 两个 if 满足,set 输出 SET 关键字。注意每个 if 内部都以逗号结尾,set 智能移除了最后一个字段后的多余逗号,最终 SQL 是干净的 SET name = ?, major = ?。
情况二:只更新 score
测试代码:
Student student = new Student();
student.setId(2);
student.setScore(91.0);
mapper.updateStudentDynamic(student);
最终生成的 SQL 语句:
UPDATE student SET score = ? WHERE id = ?
参数值: 91.0, 2
执行后表数据:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 91.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
分析: 只有 score 条件满足,set 自动插入 SET 并移除 score = ? 后面的逗号。如果只更新一个字段,逗号处理依然完美。
情况三:只传入 id,其他字段都为 null
测试代码:
Student student = new Student();
student.setId(3);
// name, age, major, score 全部为 null
mapper.updateStudentDynamic(student);
最终生成的 SQL 语句:
UPDATE student WHERE id = ?
执行结果: SQL 语法错误!UPDATE student WHERE id = ? 缺少 SET 子句。
分析: 所有 if 都不满足,set 内部没有任何内容输出,因此不输出 SET 关键字。最终 SQL 变成 UPDATE student WHERE id = ?,这是语法错误的。这说明单独使用 set 时,必须确保至少有一个字段会被更新,或者需要在 Java 代码中做前置校验。
易错场景 / 常见误区
| 误区 | 错误示例 | 后果 | 正解 |
|---|---|---|---|
if 内部不以逗号结尾 | <if> name = #{name} </if> | 多个条件满足时,SQL 变成 SET name = ? age = ?,缺少逗号分隔,语法错误 | 每个 if 内部都以逗号结尾,让 set 自动移除最后一个多余的逗号 |
| 所有字段都为 null | 只传 id,其他为 null | set 不输出任何内容,SQL 变成 UPDATE student WHERE id = ?,语法错误 | 在 Java 代码中前置校验,确保至少有一个更新字段;或使用 choose 做兜底 |
在 set 后面手写 SET | UPDATE student SET <set>...</set> | 语法错误,set 元素本身就会输出 SET | 删除手写的 SET,完全交给 set 元素处理 |
| 数值类型判断空字符串 | <if test="age != ''"> | age 是 Integer,与空字符串比较可能触发类型转换异常 | 数值类型只判断 age != null |
认为 set 能处理所有逗号 | 在条件中间写逗号 | set 只移除输出内容后缀的逗号,中间的逗号不会动 | 确保逗号只在每个字段赋值语句的末尾 |
面试考点
Q1:set 元素和 where 元素有什么相似之处?
A:
set和where都是trim元素的预设特例,设计思想完全一致。where用于 SELECT 语句,自动插入WHERE并移除前缀多余的AND/OR;set用于 UPDATE 语句,自动插入SET并移除后缀多余的逗号。它们都解决了动态 SQL 中因为条件不确定而产生的语法边界问题。
Q2:set 的底层实现等价于哪个 trim 配置?
A:
<trim prefix="SET" suffixOverrides=",">。它会在内容前添加SET前缀,并移除内容末尾多余的逗号。
Q3:如果所有更新字段都为 null,set 会生成什么样的 SQL?
A:所有
if都不满足时,set内部没有任何内容输出,因此不会输出SET关键字。最终 SQL 会变成UPDATE student WHERE id = ?,这是语法错误的。因此实际开发中需要在 Java 层做前置校验,确保至少有一个字段需要更新,或者使用choose设置一个默认更新字段作为兜底。
Q4:以下代码有什么问题?
<set> <if test="name != null">name = #{name}</if> <if test="major != null">major = #{major}</if> </set>
A:问题在于
if内部的赋值语句末尾没有加逗号。当两个条件同时满足时,生成的 SQL 是SET name = ? major = ?,两个字段之间缺少逗号分隔,导致语法错误。正确做法是在每个if内部都以逗号结尾:name = #{name},和major = #{major},,让set自动移除最后一个多余的逗号。
小结
set 是 MyBatis 动态 SQL 中专门用于 UPDATE 语句的智能包装器,它与 if 配合实现了动态字段更新。set 自动解决了两个核心问题:① 何时插入 SET 关键字;② 如何移除最后一个字段后多余的逗号。它是 trim 元素的预设特例,底层等价于 <trim prefix="SET" suffixOverrides=",">。使用 set 后,动态 UPDATE 代码变得简洁、安全、专业。
需要注意的是,当所有更新字段都为 null 时,set 不会输出任何内容,会导致 SQL 语法错误。实际开发中应在 Java 层做前置校验,或设计兜底更新字段。
下一节我们将学习 trim 元素,它是 where 和 set 的底层通用实现,提供了最灵活的动态 SQL 前缀/后缀处理能力。
下一章引子
where 和 set 虽然强大,但它们是固定行为的预设方案。如果你需要自定义前缀、后缀,或者需要同时处理前缀和后缀的多余字符,该怎么办?trim 元素是 MyBatis 动态 SQL 中最灵活的容器,它是 where 和 set 的底层通用实现。掌握 trim,你就掌握了动态 SQL 的终极武器。继续阅读,解锁动态 SQL 的完全体。