核心概念
- 通用 Mapper:
BaseMapper<T>
接口提供了deleteById
,delete
,deleteBatchIds
,deleteByMap
等通用删除方法,无需手写 SQL 即可实现基础删除。 - 物理删除 vs 逻辑删除:
- 物理删除:直接从数据库表中移除记录(
DELETE FROM ...
)。这是默认行为。 - 逻辑删除:不真正删除数据,而是通过更新一个“删除状态”字段(如
deleted=1
)来标记记录为已删除。查询时自动过滤已删除记录。需通过@TableLogic
注解和全局配置启用。
- 物理删除:直接从数据库表中移除记录(
- 方法区别:
deleteById(ID id)
:根据主键 ID 删除单条记录。delete(Wrapper<T> wrapper)
:根据复杂条件(Wrapper)删除多条记录。deleteBatchIds(Collection<? extends Serializable> idList)
:根据主键 ID 集合批量删除多条记录。deleteByMap(Map<String, Object> columnMap)
:根据列名和值的 Map 进行删除(相当于WHERE column1=value1 AND column2=value2 ...
)。
操作步骤 (非常详细)
前提:已正确配置 MyBatis-Plus、数据源、Mapper 接口继承 BaseMapper<T>
,并注入 Mapper Bean。
场景 1: 根据 ID 删除单条记录 (deleteById
)
@Service
public class UserService {
@Autowired
private UserMapper userMapper; // 注入 Mapper
/**
* 根据用户 ID 删除用户
* @param userId 用户 ID
* @return 删除的记录数 (通常为 0 或 1)
*/
public int deleteUserById(Long userId) {
// 1. 调用 BaseMapper 的 deleteById 方法
int result = userMapper.deleteById(userId);
// 2. result 为删除的行数
if (result > 0) {
System.out.println("成功删除用户,ID: " + userId);
} else {
System.out.println("未找到用户或删除失败,ID: " + userId);
}
return result;
}
}
场景 2: 根据条件删除多条记录 (delete
)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 删除指定邮箱域名下的所有用户
* @param emailDomain 邮箱域名,如 "example.com"
* @return 删除的记录数
*/
public int deleteUserByEmailDomain(String emailDomain) {
// 1. 创建 QueryWrapper,构建删除条件
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 2. 添加条件:email 字段以指定域名结尾
wrapper.likeRight("email", "@" + emailDomain); // 或使用 lambda: wrapper.lambda().likeRight(User::getEmail, "@" + emailDomain);
// 3. 调用 delete 方法
int result = userMapper.delete(wrapper);
System.out.println("成功删除 " + result + " 个用户,邮箱域名: " + emailDomain);
return result;
}
/**
* 删除状态为 INACTIVE 且创建时间早于某个日期的用户
* @param thresholdDate 日期阈值
* @return 删除的记录数
*/
public int deleteInactiveOldUsers(LocalDateTime thresholdDate) {
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.eq(User::getStatus, "INACTIVE")
.lt(User::getCreateTime, thresholdDate);
return userMapper.delete(lambdaWrapper);
}
}
场景 3: 根据 ID 集合批量删除 (deleteBatchIds
)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 批量删除用户
* @param userIdList 用户 ID 列表
* @return 删除的记录数
*/
public int deleteUsersBatch(List<Long> userIdList) {
// 1. 检查集合是否为空
if (userIdList == null || userIdList.isEmpty()) {
System.out.println("ID 列表为空,无需删除。");
return 0;
}
// 2. 调用 deleteBatchIds 方法
int result = userMapper.deleteBatchIds(userIdList);
System.out.println("成功批量删除 " + result + " 个用户。");
return result;
}
}
场景 4: 根据列值 Map 删除 (deleteByMap
)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 删除指定部门和职位的用户
* @param department 部门
* @param position 职位
* @return 删除的记录数
*/
public int deleteUserByDeptAndPosition(String department, String position) {
// 1. 创建 Map 存储列名和值
Map<String, Object> columnMap = new HashMap<>();
columnMap.put("department", department);
columnMap.put("position", position);
// 2. 调用 deleteByMap 方法
// 注意:Map 的 key 是数据库列名 (非 Java 属性名),除非 map-underscore-to-camel-case=false
int result = userMapper.deleteByMap(columnMap);
System.out.println("成功删除 " + result + " 个用户,部门: " + department + ", 职位: " + position);
return result;
}
}
常见错误
NullPointerException
:- 原因:
userMapper
未被正确注入(@Autowired
失败)。 - 解决:确保 Service 类被
@Service
注解,Mapper 被正确扫描(@MapperScan
),且包路径正确。
- 原因:
- 删除了意外的记录:
- 原因:
delete(Wrapper<T>)
或deleteByMap
构建的条件不精确或有误。 - 解决:在执行删除前,务必先用
selectList(wrapper)
或selectCount(wrapper)
测试查询条件是否正确! 仔细检查wrapper
的拼接逻辑。
- 原因:
deleteBatchIds
删除数量不符:- 原因:
idList
中包含不存在的 ID 或null
值。 - 解决:
deleteBatchIds
会忽略null
,并对每个 ID 执行删除,返回实际删除的行数。预期删除数可能小于idList
大小。检查 ID 是否存在。
- 原因:
- 逻辑删除未生效:
- 原因:未正确配置逻辑删除(全局配置
logic-delete-field
和实体类@TableLogic
),或MybatisPlusInterceptor
未包含BlockAttackInnerInterceptor
(防止全表删除) 导致物理删除被阻止。 - 解决:检查
application.yml
中的mybatis-plus.global-config.db-config
相关配置和实体类@TableLogic
注解。确认插件配置。
- 原因:未正确配置逻辑删除(全局配置
deleteByMap
使用 Java 属性名:- 原因:
columnMap
的 key 使用了 Java 属性名(如userName
),但数据库列名是user_name
,且map-underscore-to-camel-case
为true
时,MyBatis-Plus 可能无法正确映射。 - 解决:
deleteByMap
的 key 必须使用数据库列名(如user_name
),除非明确知道映射规则。使用QueryWrapper
通常更安全。
- 原因:
- 误删全表 (全表删除):
- 原因:调用
delete(new QueryWrapper<>())
且未启用BlockAttackInnerInterceptor
。 - 解决:绝对避免创建一个无任何条件的
QueryWrapper
并用于删除!生产环境务必启用BlockAttackInnerInterceptor
插件来阻止全表更新/删除。
- 原因:调用
注意事项
- 返回值:所有删除方法均返回
int
类型,表示数据库中实际被删除(或标记删除)的行数。不等于 0 表示成功删除至少一行。 - 逻辑删除优先:如果启用了逻辑删除,上述所有删除方法都会自动转换为
UPDATE
语句,更新logic-delete-field
字段的值,而不是执行DELETE
。查询时也会自动添加WHERE deleted=0
条件。 deleteByMap
的局限性:只能构建AND
连接的等值 (=
) 条件。无法构建OR
、LIKE
、范围查询等复杂条件。对于复杂条件,强烈推荐使用delete(Wrapper<T>)
。deleteBatchIds
的性能:底层通常生成DELETE FROM table WHERE id IN (?, ?, ?)
语句,性能较好。但idList
过大(如超过几千)时需注意数据库IN
子句的长度限制和性能。- 事务管理:删除操作通常需要事务保证。在
@Service
方法上添加@Transactional
注解。 - 主键类型:
deleteById
和deleteBatchIds
的 ID 类型需与实体类@TableId
定义的类型匹配(如Long
,String
)。 - Wrapper 的泛型:创建
QueryWrapper
时,指定泛型QueryWrapper<User>
能获得更好的类型安全和 Lambda 表达式支持。
使用技巧
LambdaWrapper
:优先使用LambdaQueryWrapper<User>
,避免硬编码字符串,提高代码可读性和重构安全性。LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getId, userId).eq(User::getStatus, "ACTIVE"); userMapper.delete(wrapper);
- 条件预检查:对于
delete
和deleteByMap
,先用selectCount
确认要删除的记录数,避免误操作。long count = userMapper.selectCount(wrapper); if (count > 0 && confirmDeletion(count)) { // 业务确认逻辑 userMapper.delete(wrapper); }
- 结合
@Transactional
:在 Service 方法上使用@Transactional(rollbackFor = Exception.class)
确保操作的原子性。 - 处理返回值:检查返回的
int
值,根据业务需求判断是否删除成功(如预期删除 1 行但返回 0,可能表示数据不存在或已被删除)。 wrapper.last()
谨慎使用:虽然可以用wrapper.last("LIMIT 1")
限制删除数量,但可能破坏分页等插件功能,且不够直观,不推荐用于删除。
最佳实践与性能优化
- 首选
deleteById
和deleteBatchIds
:对于基于主键的删除,它们最直接、高效且安全。 - 慎用
deleteByMap
:仅在条件简单且明确为等值AND
时使用。复杂条件一律使用Wrapper
。 delete(Wrapper)
用于复杂条件:这是处理非主键复杂删除条件的标准方式。- 强制启用
BlockAttackInnerInterceptor
:生产环境必须配置,防止因代码 Bug 导致全表数据被清空。@Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防止全表删除/更新 // ... 其他插件 return interceptor; } }
- 批量删除大集合:
- 如果
idList
非常大(如数万条),考虑分批处理(例如每次 1000 条),避免单次 SQL 过长或事务过大。 - 或者,如果条件能用 SQL 表达,优先使用
delete(Wrapper)
一条 SQL 完成。
- 如果
- 性能监控:关注删除操作的执行时间,特别是
delete(Wrapper)
的复杂查询条件,确保相关字段有合适的索引。 - 软删除(逻辑删除)策略:
- 除非有明确的合规或审计要求必须物理删除,否则优先考虑逻辑删除,避免数据丢失。
- 定期归档或物理清理(
DELETE
)已逻辑删除且过期很久的数据,以控制表大小。
- 日志记录:在执行重要删除操作前后,记录日志(如删除的 ID、数量、操作人),便于审计和问题排查。