1. MyBatis-Plus逻辑删除机制解析
逻辑删除是业务系统中常见的数据处理方式,与物理删除相比,它通过标记字段实现"软删除",保留了数据完整性和可追溯性。MyBatis-Plus作为MyBatis的增强工具,提供了开箱即用的逻辑删除解决方案。
1.1 逻辑删除的核心实现原理
在数据库设计中,我们通常会添加一个标记字段(如is_deleted、del_flag)来表示记录是否被删除。MyBatis-Plus通过拦截器机制,自动在所有查询操作中附加该字段的过滤条件,同时在删除操作时将其转换为更新操作。
实现原理主要涉及三个关键组件:
- 逻辑删除注解:@TableLogic用于标识逻辑删除字段
- SQL解析器:自动修改SQL语句,添加删除标记条件
- 元对象处理器:在插入时自动填充逻辑删除字段默认值
重要提示:逻辑删除字段建议使用Integer或Boolean类型,避免使用字符串等复杂类型,以免影响查询性能。
1.2 基础配置方法
在Spring Boot项目中配置逻辑删除只需三步:
- 数据库表添加标记字段:
sql复制ALTER TABLE user ADD COLUMN deleted TINYINT(1) DEFAULT 0 COMMENT '删除标记';
- 实体类添加注解:
java复制@TableLogic
private Integer deleted;
- 全局配置(可选):
yaml复制mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-not-delete-value: 0 # 未删除值
logic-delete-value: 1 # 已删除值
2. 五种查询处理方案详解
2.1 基础查询自动过滤
配置逻辑删除后,所有继承BaseMapper的查询方法都会自动过滤已删除数据。例如:
java复制userMapper.selectById(1);
// 实际执行SQL: SELECT * FROM user WHERE id=1 AND deleted=0
这种方式的优点是零编码实现,但需要注意:
- 只对MyBatis-Plus内置方法生效
- 自定义SQL需要手动处理删除标记
- 联表查询时需特别注意关联表的过滤条件
2.2 包含已删除数据的查询
有时我们需要查询包含已删除数据的完整结果集,有三种实现方式:
方法一:使用@SqlParser注解
java复制@SqlParser(filter = true)
public List<User> selectAllIncludeDeleted() {
return userMapper.selectList(null);
}
方法二:自定义SQL忽略逻辑删除
xml复制<select id="selectAll" resultType="User">
SELECT * FROM user
</select>
方法三:使用条件构造器手动覆盖
java复制LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.apply("1=1"); // 覆盖自动添加的deleted条件
2.3 仅查询已删除数据
针对回收站等场景,我们需要专门查询已被删除的记录:
java复制LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getDeleted, 1);
List<User> deletedUsers = userMapper.selectList(wrapper);
2.4 联表查询处理方案
在多表关联查询时,需要特别注意各表的逻辑删除状态。推荐两种方案:
方案一:手动添加关联表条件
xml复制<select id="selectUserWithPosts" resultMap="userWithPosts">
SELECT u.*, p.*
FROM user u LEFT JOIN post p ON u.id = p.user_id
WHERE u.deleted = 0 AND p.deleted = 0
</select>
方案二:使用子查询过滤
java复制LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.inSql(User::getId, "SELECT user_id FROM post WHERE deleted = 0");
2.5 统计查询的特殊处理
进行count等统计查询时,逻辑删除可能导致结果不符合预期:
java复制// 统计有效用户数
Long activeCount = userMapper.selectCount(null); // 自动排除deleted=1的记录
// 统计全部用户数(包含已删除)
Long totalCount = userMapper.selectCount(
new QueryWrapper<User>().apply("1=1")
);
3. 逻辑删除的进阶应用技巧
3.1 多租户与逻辑删除的协同处理
在多租户系统中,需要同时考虑tenant_id和deleted字段的过滤:
java复制LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getTenantId, currentTenantId)
.eq(User::getDeleted, 0);
建议在自定义的TenantHandler中统一处理租户和逻辑删除条件。
3.2 历史数据归档方案
对于已删除但需要长期保留的数据,推荐采用归档策略:
- 定期将deleted=1的数据迁移到历史表
- 使用数据库事件或Spring定时任务实现
- 归档后从主表物理删除
示例归档SQL:
sql复制INSERT INTO user_history
SELECT * FROM user WHERE deleted = 1 AND update_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
DELETE FROM user WHERE id IN (SELECT id FROM user_history);
3.3 性能优化建议
逻辑删除可能带来的性能问题及解决方案:
- 索引优化:确保deleted字段有适当索引
sql复制ALTER TABLE user ADD INDEX idx_deleted (deleted);
- 查询优化:避免在deleted字段上使用!=操作
java复制// 不推荐
wrapper.ne(User::getDeleted, 1);
// 推荐明确状态
wrapper.eq(User::getDeleted, 0);
- 定期维护:对已删除数据量大的表进行归档或分区
4. 常见问题排查指南
4.1 逻辑删除失效的排查步骤
当发现逻辑删除不生效时,可按以下步骤排查:
- 检查实体类字段是否添加@TableLogic注解
- 确认全局配置是否正确加载(检查application.yml)
- 检查是否使用了自定义SQL而未添加deleted条件
- 查看生成的SQL日志确认条件是否被添加
4.2 与乐观锁的冲突解决
当同时使用@Version乐观锁和@TableLogic时,更新操作可能报错。解决方案:
java复制// 更新前先查询确保对象状态正确
User user = userMapper.selectById(id);
user.setStatus(newStatus);
userMapper.updateById(user);
4.3 事务中的特殊处理
在事务中处理逻辑删除需注意:
- 逻辑删除操作会转换为update,需要确保事务传播行为正确
- 批量逻辑删除建议使用事务批量操作
java复制@Transactional
public void batchDelete(List<Long> ids) {
ids.forEach(id -> {
User user = new User();
user.setId(id);
user.setDeleted(1);
userMapper.updateById(user);
});
}
5. 最佳实践与替代方案
5.1 字段设计建议
- 使用tinyint(1)而非varchar作为删除标记字段
- 考虑添加delete_time字段记录删除时间
- 重要数据可添加delete_user记录操作人
完整字段示例:
java复制@TableLogic
private Integer deleted;
private LocalDateTime deleteTime;
private String deleteUser;
5.2 状态机模式替代方案
对于复杂状态流转的业务,可以考虑使用状态机模式替代简单逻辑删除:
java复制public enum UserState {
ACTIVE(0), DELETED(1), ARCHIVED(2);
private final int code;
// ...
}
// 查询时根据状态过滤
wrapper.eq(User::getState, UserState.ACTIVE.code);
5.3 审计日志结合方案
重要数据删除建议结合审计日志:
- 逻辑删除前记录操作日志
- 使用Spring AOP或MyBatis插件实现
- 日志包含删除时间、操作人、原始数据等
java复制@Around("execution(* com..mapper.*.delete*(..))")
public Object logDeleteOperation(ProceedingJoinPoint pjp) {
// 记录前置日志
Object result = pjp.proceed();
// 记录后置日志
return result;
}
在实际项目中,我通常会根据数据重要性决定是否采用逻辑删除。对于核心业务数据,逻辑删除+审计日志是标配;而对于临时数据或缓存数据,物理删除可能更合适。另外要注意定期归档已删除数据,避免主表数据膨胀影响查询性能。
