一、核心概念
UpdateWrapper<T>
是 MyBatis-Plus 框架中的一个条件构造器(Wrapper),专门用于构建 UPDATE
语句的 SET 和 WHERE 子句。它允许您以面向对象的方式动态地构造更新条件,极大地提高了代码的可读性和可维护性。
- 泛型
T
: 代表您要操作的实体类类型(Entity Class)。 - SET 子句: 通过
set()
方法或其变体来指定要更新的字段和值。 - WHERE 子句: 通过一系列
eq
,ne
,gt
,ge
,lt
,le
,like
,in
,isNull
,isNotNull
等方法来构建查询条件。 - 链式调用:
UpdateWrapper
的所有方法都支持链式调用,使得代码非常流畅。 - 与
LambdaUpdateWrapper
的区别:UpdateWrapper
使用字段名字符串(如"name"
),而LambdaUpdateWrapper
使用 Lambda 表达式(如User::getName
),后者在编译时更安全(避免字段名拼写错误),但UpdateWrapper
在动态字段名场景下更灵活。
二、详细操作步骤
以下是使用 UpdateWrapper
进行数据库更新的完整、详细步骤:
步骤 1: 引入依赖
确保您的项目中已正确引入 MyBatis-Plus 的依赖。以 Maven 为例:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version> <!-- 请使用最新稳定版本 -->
</dependency>
步骤 2: 创建实体类 (Entity)
定义一个与数据库表对应的 Java 实体类,并使用 MyBatis-Plus 注解。
import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@TableName("user") // 指定对应的数据库表名
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO) // 主键
private Long id;
@TableField("name")
private String name;
@TableField("age")
private Integer age;
@TableField("email")
private String email;
@TableField("status")
private Integer status;
@TableField(fill = FieldFill.INSERT) // 创建时间,插入时填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 更新时间,插入和更新时填充
private LocalDateTime updateTime;
// 构造函数、Getter、Setter 方法
public User() {}
public User(String name, Integer age, String email, Integer status) {
this.name = name;
this.age = age;
this.email = email;
this.status = status;
}
// ... (省略 Getter 和 Setter)
}
步骤 3: 创建 Mapper 接口
创建一个继承自 BaseMapper<T>
的 Mapper 接口。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// BaseMapper 已经提供了基本的 CRUD 方法,包括 update 方法
}
步骤 4: 在 Service 或 Controller 中使用 UpdateWrapper
这是核心步骤。以下是几种常见的更新场景:
场景 1: 根据 ID 更新特定字段
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 根据用户ID更新用户的姓名和邮箱
* @param userId 用户ID
* @param newName 新姓名
* @param newEmail 新邮箱
* @return 更新成功的记录数
*/
public int updateUserById(Long userId, String newName, String newEmail) {
// 1. 创建 UpdateWrapper 实例,指定实体类型
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
// 2. 设置要更新的字段 (SET 子句)
// set(fieldName, value): 设置字段名为 fieldName 的值为 value
updateWrapper.set("name", newName); // 直接使用数据库字段名
updateWrapper.set("email", newEmail);
// 3. 设置更新条件 (WHERE 子句)
// eq(column, value): column = value
updateWrapper.eq("id", userId);
// 4. 执行更新操作
// userMapper.update(entity, updateWrapper)
// 注意: 第一个参数 entity 通常传 null,因为 SET 子句由 wrapper 定义
// 如果传入一个 User 对象,其非 null 字段也会被加入 SET 子句,可能产生意外
int rowsAffected = userMapper.update(null, updateWrapper);
return rowsAffected;
}
}
场景 2: 根据复杂条件更新多个记录
/**
* 将年龄大于等于 minAge 且状态为 status 的用户的状态更新为 newStatus
* @param minAge 最小年龄
* @param status 原状态
* @param newStatus 新状态
* @return 更新成功的记录数
*/
public int updateUsersByAgeAndStatus(Integer minAge, Integer status, Integer newStatus) {
// 1. 创建 UpdateWrapper
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
// 2. 设置要更新的字段
updateWrapper.set("status", newStatus);
// 3. 构建复杂的 WHERE 条件
// ge(column, value): column >= value
// and(): 添加 AND 条件
// eq(column, value): column = value
updateWrapper.ge("age", minAge)
.and()
.eq("status", status);
// 4. 执行更新
return userMapper.update(null, updateWrapper);
}
圔景 3: 使用逻辑删除字段更新(结合 set
和 setSql
)
/**
* 逻辑删除用户(将 status 字段设置为 0,表示已删除)
* 并更新 updateTime 字段
* @param userId 用户ID
* @return 更新成功的记录数
*/
public int softDeleteUser(Long userId) {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
// 使用 set 设置 status
updateWrapper.set("status", 0);
// 使用 setSql 可以直接写 SQL 片段,这里设置 updateTime 为数据库当前时间
// 注意:setSql 是直接拼接到 SET 子句的,需确保 SQL 正确
updateWrapper.setSql("update_time = NOW()");
// 更新条件
updateWrapper.eq("id", userId);
return userMapper.update(null, updateWrapper);
}
场景 4: 动态条件更新(根据参数是否为空决定是否添加条件)
/**
* 根据可选条件更新用户信息
* @param user 包含更新信息的 User 对象(id 必须有,其他字段可选)
* @param conditionWrapper 包含可选查询条件的 UpdateWrapper (用于构建 WHERE)
* @return 更新成功的记录数
*/
public int updateUserWithOptionalConditions(User user, UpdateWrapper<User> conditionWrapper) {
// 1. 创建主 UpdateWrapper 用于 SET
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
// 2. 动态添加 SET 字段 (只有非 null 的字段才更新)
if (user.getName() != null) {
updateWrapper.set("name", user.getName());
}
if (user.getAge() != null) {
updateWrapper.set("age", user.getAge());
}
if (user.getEmail() != null) {
updateWrapper.set("email", user.getEmail());
}
if (user.getStatus() != null) {
updateWrapper.set("status", user.getStatus());
}
// 注意:createTime 通常不在此处更新,由数据库或填充策略处理
// 3. 将传入的条件 Wrapper 合并到主 Wrapper 的 WHERE 子句中
// 可以使用 and(Consumer<Wrapper<T>> consumer) 方法
if (conditionWrapper != null) {
// 使用 and() 将 conditionWrapper 的所有条件包裹在括号内,并与前面的条件用 AND 连接
// 例如:... WHERE (existing_conditions) AND (conditionWrapper_conditions)
updateWrapper.and(conditionWrapper);
// 或者,如果希望直接合并(不加括号),可以考虑遍历 conditionWrapper 的条件并手动添加
// 但 and() 方法更安全,避免逻辑错误。
} else {
// 如果没有额外条件,至少需要一个主键条件,否则会更新所有记录!
// 这里假设 user 对象的 id 是主键且不为 null
if (user.getId() != null) {
updateWrapper.eq("id", user.getId());
} else {
throw new IllegalArgumentException("用户ID不能为空");
}
}
// 4. 执行更新
return userMapper.update(null, updateWrapper);
}
// 调用示例:
// User userToUpdate = new User();
// userToUpdate.setId(1L);
// userToUpdate.setName("NewName");
// // userToUpdate.setAge(null); // 年龄不更新
//
// UpdateWrapper<User> condWrapper = new UpdateWrapper<>();
// condWrapper.gt("age", 18).eq("status", 1); // 只更新年龄>18且状态为1的用户
//
// int result = userService.updateUserWithOptionalConditions(userToUpdate, condWrapper);
三、常见错误
忘记设置 WHERE 条件:
- 错误:
UpdateWrapper<User> uw = new UpdateWrapper<>(); uw.set("status", 0); userMapper.update(null, uw);
- 后果: 这会更新 表中的所有记录,非常危险!
- 解决: 务必使用
eq
,in
等方法设置明确的WHERE
条件。如果需要更新所有记录,应明确写出updateWrapper.last("WHERE 1=1")
或类似,但要极其谨慎。
- 错误:
在
update
方法中错误地传入 Entity:- 错误:
userMapper.update(user, updateWrapper);
其中user
对象有非 null 字段。 - 后果:
user
对象的非 null 字段也会被加入SET
子句,可能导致意外覆盖。UpdateWrapper
的set
方法设置的值会覆盖user
对象中的同名字段值,但逻辑复杂且易混淆。 - 解决: 当使用
UpdateWrapper
定义SET
子句时,第一个参数传null
。updateWrapper
本身已经定义了所有要更新的字段。
- 错误:
字段名拼写错误:
- 错误:
updateWrapper.eq("user_name", "John");
但数据库字段实际是name
。 - 后果: 条件不生效,可能更新不到数据或更新错误的数据。
- 解决: 仔细核对数据库字段名。考虑使用
LambdaUpdateWrapper
避免此问题。
- 错误:
set
方法参数顺序错误:- 错误:
updateWrapper.set(true, "status");
(参数顺序反了) - 后果: 语法错误,编译不通过。
- 解决: 记住
set(column, value)
。
- 错误:
未处理空值导致条件失效:
- 错误: 在动态构建条件时,
if (name != null && !name.isEmpty()) updateWrapper.eq("name", name);
但忘记处理空字符串。 - 后果: 如果
name
传了空字符串""
,条件不会被添加,可能导致范围过大。 - 解决: 在添加条件前,对参数进行充分的空值和空字符串校验。
- 错误: 在动态构建条件时,
四、注意事项
- 安全第一: 永远不要在生产环境执行没有
WHERE
条件的UPDATE
操作。UpdateWrapper
无法阻止您构建无WHERE
的更新,必须由开发者保证。 update
方法参数: 明确BaseMapper.update(T entity, Wrapper<T> updateWrapper)
的语义。当使用UpdateWrapper
控制SET
时,entity
参数应为null
。- 事务: 更新操作通常需要事务保证数据一致性。确保您的
Service
方法被@Transactional
注解修饰。 - 返回值:
update
方法返回int
,表示受影响的行数。检查返回值可以判断更新是否成功(例如,期望更新1行却返回0,可能意味着条件不匹配)。 - 并发更新: 在高并发场景下,直接更新可能遇到并发问题。考虑使用版本号乐观锁(
@Version
注解)或数据库悲观锁。 - SQL 注入:
UpdateWrapper
内部使用PreparedStatement
,其方法参数(如eq("name", userInput)
)会被安全地参数化,有效防止 SQL 注入。但是,setSql()
方法接收原始 SQL 字符串,如果拼接了用户输入,极易导致 SQL 注入!绝对避免setSql("field = " + userInput)
。如果必须动态 SQL,使用set()
或其他条件方法。
五、使用技巧
- 链式调用: 充分利用链式调用,让代码更简洁流畅。
updateWrapper.set("status", 1).set("update_time", now).eq("id", id).gt("version", currentVersion);
lambda()
方法:UpdateWrapper
有一个lambda()
方法,可以将其转换为LambdaUpdateWrapper
,获得类型安全。// 从 UpdateWrapper 转换 UpdateWrapper<User> uw = new UpdateWrapper<>(); LambdaUpdateWrapper<User> luw = uw.lambda(); luw.set(User::getStatus, 1).eq(User::getId, userId); // 字段名安全
last()
方法: 将 SQL 片段添加到 SQL 语句的末尾。慎用,因为它可能破坏查询的可移植性或引入安全风险。updateWrapper.set("status", 0).eq("id", id).last("LIMIT 1"); // 添加 LIMIT
apply()
方法: 拼接 SQL 片段,支持?
占位符(安全)。// 安全地拼接 updateWrapper.apply("date_format(create_time, '%Y-%m') = {0}", "2023-10");
nested()
方法: 创建嵌套条件,相当于加括号。// WHERE (name = 'John' OR name = 'Jane') AND status = 1 updateWrapper.nested(iw -> iw.eq("name", "John").or().eq("name", "Jane")) .eq("status", 1);
set
的变体:set(boolean condition, String column, Object val)
: 只有当condition
为true
时才添加SET
子句。非常适合动态更新。updateWrapper.set(StringUtils.isNotBlank(newName), "name", newName) .set(newAge != null, "age", newAge);
setSql(String sql)
: 直接添加 SQL 片段到SET
,注意 SQL 注入。
六、最佳实践与性能优化
- 优先使用
LambdaUpdateWrapper
: 在字段名确定的场景下,优先使用LambdaUpdateWrapper
,利用编译时检查避免字段名错误,提高代码健壮性。 - 明确且最小化更新范围:
WHERE
条件应尽可能精确,避免不必要的全表扫描或锁定过多行。 - 批量更新: 如果需要更新多条记录,且每条记录的更新值不同,考虑使用
SqlSessionTemplate
的update
方法配合ExecutorType.BATCH
,或者 MyBatis-Plus 的Service
层的updateBatchById
方法(如果主键已知且更新字段相同,可用update(entity, updateWrapper)
配合in
条件)。 - 利用数据库索引: 确保
WHERE
子句中使用的字段(特别是等值、范围查询字段)上有合适的数据库索引,以加速条件查找。 - 避免大事务: 将大范围的更新操作拆分成小批次进行,减少单次事务的持有时间和锁竞争。
- 监控与日志: 开启 MyBatis 的 SQL 日志(如
logging.level.com.your.mapper=DEBUG
),观察生成的 SQL 语句是否符合预期,是否有性能问题。 - 结合
@TableField(fill = ...)
: 对于create_time
,update_time
等字段,使用FieldFill
策略由 MyBatis-Plus 自动填充,减少手动设置,保证一致性。 - 处理更新失败: 检查
update
方法的返回值。如果期望更新 N 行但返回值小于 N,需要处理这种情况(如抛出业务异常、记录日志等)。 - 考虑使用
updateById(entity)
: 如果只是根据主键更新实体对象中所有非 null 字段,直接使用BaseMapper.updateById(entity)
更简单高效,无需UpdateWrapper
。