1. 为什么需要逻辑删除?
在业务系统中,数据删除通常有两种方式:物理删除和逻辑删除。物理删除是指直接从数据库中删除记录,这种方式简单直接,但会带来几个严重问题:
- 数据丢失风险:一旦删除就无法恢复,如果误操作可能导致严重后果
- 历史追溯困难:无法查询已删除的数据,影响业务分析和审计
- 关联数据断裂:如果其他表有外键关联,可能导致数据不一致
逻辑删除则是一种更安全的方案,它通过在表中添加一个状态字段(如is_deleted)来标记记录是否被删除,而不是真正从数据库移除数据。MyBatis Plus作为MyBatis的增强工具,提供了开箱即用的逻辑删除功能,可以让我们以最小的成本实现这一重要特性。
实际项目中,90%以上的删除操作都应该使用逻辑删除而非物理删除,这是企业级应用开发的基本规范。
2. SpringBoot3集成MyBatis Plus基础配置
2.1 环境准备与依赖引入
首先确保你的项目是基于SpringBoot3构建的,在pom.xml中添加以下依赖:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
对于数据库连接,推荐使用HikariCP连接池:
xml复制<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
2.2 基础配置示例
在application.yml中配置基本参数:
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/your_db?useSSL=false&serverTimezone=UTC
username: root
password: your_password
hikari:
maximum-pool-size: 20
minimum-idle: 5
3. MyBatis Plus逻辑删除实现详解
3.1 数据库表设计
要实现逻辑删除,首先需要在表中添加状态字段。常见的字段设计有两种方式:
- 布尔类型(推荐):
sql复制ALTER TABLE user ADD COLUMN is_deleted TINYINT(1) DEFAULT 0 COMMENT '0-未删除 1-已删除';
- 时间戳类型(适用于需要记录删除时间的场景):
sql复制ALTER TABLE user ADD COLUMN delete_time DATETIME DEFAULT NULL COMMENT '删除时间';
3.2 实体类注解配置
在实体类中,使用@TableLogic注解标记逻辑删除字段:
java复制@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
@TableLogic
private Integer isDeleted; // 1表示删除,0表示未删除
}
注意:字段类型和注解要匹配。如果数据库字段是datetime类型,实体类字段应为LocalDateTime,注解同样适用。
3.3 全局配置(可选)
在application.yml中可以添加全局配置:
yaml复制mybatis-plus:
global-config:
db-config:
logic-delete-field: isDeleted # 全局逻辑删除字段名
logic-not-delete-value: 0 # 未删除值
logic-delete-value: 1 # 已删除值
这种配置方式可以避免在每个实体类都添加@TableLogic注解,适合字段命名规范统一的项目。
4. 逻辑删除的实际效果与原理
4.1 查询操作自动过滤
启用逻辑删除后,所有查询操作都会自动加上未删除的条件。例如:
java复制userMapper.selectList(null);
实际执行的SQL会变成:
sql复制SELECT * FROM user WHERE is_deleted = 0
4.2 删除操作变为更新
当调用deleteById方法时:
java复制userMapper.deleteById(1L);
实际执行的SQL是:
sql复制UPDATE user SET is_deleted = 1 WHERE id = 1 AND is_deleted = 0
4.3 自定义SQL的处理
如果你在XML中编写了自定义SQL,需要注意逻辑删除条件不会自动添加。有两种解决方案:
- 手动添加条件:
xml复制<select id="selectCustom" resultType="User">
SELECT * FROM user
WHERE is_deleted = 0
AND username LIKE #{pattern}
</select>
- 使用MyBatis Plus的SQL注入器(高级用法):
java复制@InterceptorIgnore(tenantLine = "true", logicDelete = "false")
5. 进阶使用技巧与常见问题
5.1 查询包含已删除数据
有时我们需要查询所有数据(包括已删除的),可以通过以下方式实现:
java复制// 方式1:使用Wrapper忽略逻辑删除
userMapper.selectList(Wrappers.<User>lambdaQuery()
.ignoreLogicDel()
.eq(User::getUsername, "test"));
// 方式2:直接使用SQL注入
@Select("SELECT * FROM user WHERE username = #{name}")
List<User> selectAllIncludeDeleted(@Param("name") String name);
5.2 物理删除的实现
在必须物理删除的场景下,可以这样操作:
java复制// 方式1:使用自定义SQL
@Delete("DELETE FROM user WHERE id = #{id}")
int physicalDeleteById(@Param("id") Long id);
// 方式2:使用条件构造器
userMapper.delete(Wrappers.<User>lambdaQuery()
.apply("id = {0} AND is_deleted = 1", id));
5.3 唯一索引冲突问题
逻辑删除可能导致唯一索引冲突。例如用户表中username有唯一索引,当用户被删除后,再创建相同用户名的记录会报错。解决方案:
- 修改唯一索引包含删除状态:
sql复制ALTER TABLE user ADD UNIQUE INDEX uk_username_deleted (username, is_deleted);
- 使用删除时间替代布尔值:
sql复制ALTER TABLE user ADD UNIQUE INDEX uk_username (username, delete_time);
5.4 关联查询处理
当多表关联查询时,逻辑删除需要特别处理。例如用户和订单关联查询:
java复制@Select("SELECT u.*, o.* FROM user u LEFT JOIN order o ON u.id = o.user_id " +
"WHERE u.is_deleted = 0 AND (o.id IS NULL OR o.is_deleted = 0)")
List<UserOrderVO> selectUserWithOrders();
6. 性能优化建议
6.1 索引优化
确保逻辑删除字段有适当的索引:
sql复制ALTER TABLE user ADD INDEX idx_deleted (is_deleted);
对于大型表,可以考虑使用更高效的枚举类型:
sql复制ALTER TABLE user MODIFY COLUMN is_deleted ENUM('0','1') DEFAULT '0';
6.2 定期归档策略
长期积累的逻辑删除数据会影响性能,建议实现归档机制:
java复制@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void archiveDeletedData() {
// 1. 查询30天前删除的数据
LocalDateTime date = LocalDateTime.now().minusDays(30);
List<User> deletedUsers = userMapper.selectList(Wrappers.<User>lambdaQuery()
.eq(User::getIsDeleted, 1)
.le(User::getUpdateTime, date));
// 2. 备份到历史表
backupMapper.batchInsert(deletedUsers);
// 3. 物理删除
userMapper.delete(Wrappers.<User>lambdaQuery()
.eq(User::getIsDeleted, 1)
.le(User::getUpdateTime, date));
}
6.3 批量操作优化
对于批量逻辑删除,使用Service的批量方法更高效:
java复制userService.removeBatchByIds(ids); // 比循环调用deleteById性能更好
对应的SQL是:
sql复制UPDATE user SET is_deleted = 1 WHERE id IN (1, 2, 3) AND is_deleted = 0
7. 测试策略与注意事项
7.1 单元测试要点
测试逻辑删除功能时,需要覆盖以下场景:
java复制@Test
public void testLogicDelete() {
// 1. 新增测试数据
User user = new User();
user.setUsername("testUser");
userMapper.insert(user);
// 2. 逻辑删除
userMapper.deleteById(user.getId());
// 3. 验证查询不到
User deleted = userMapper.selectById(user.getId());
assertNull(deleted);
// 4. 验证包含已删除的查询
List<User> all = userMapper.selectList(Wrappers.<User>lambdaQuery().ignoreLogicDel());
assertFalse(all.isEmpty());
}
7.2 集成测试要点
在Controller层测试时,注意前后端交互:
java复制@Test
public void testDeleteAPI() throws Exception {
// 创建测试用户
User user = userService.save(new UserDTO("apiUser", "pwd"));
// 调用删除API
mockMvc.perform(delete("/users/" + user.getId()))
.andExpect(status().isOk());
// 验证状态变更
User deleted = userMapper.selectOne(Wrappers.<User>lambdaQuery()
.eq(User::getId, user.getId())
.ignoreLogicDel());
assertEquals(1, deleted.getIsDeleted());
}
7.3 生产环境注意事项
- 权限控制:确保只有授权用户能查询已删除数据
- 审计日志:记录所有删除操作的关键信息
- 数据恢复:提供恢复已删除数据的接口
- 性能监控:关注逻辑删除对查询性能的影响
8. 与其他特性的整合
8.1 与乐观锁整合
逻辑删除常与@Version乐观锁一起使用:
java复制@Data
public class User {
@TableId
private Long id;
@Version
private Integer version;
@TableLogic
private Integer isDeleted;
}
8.2 与多租户整合
在SAAS系统中,逻辑删除需要与多租户条件配合:
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor());
interceptor.addInnerInterceptor(new LogicSqlInjector());
return interceptor;
}
8.3 与字段自动填充整合
自动填充删除时间:
java复制@Data
public class User {
@TableLogic
private Integer isDeleted;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime deleteTime;
}
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void updateFill(MetaObject metaObject) {
Integer deleted = (Integer) metaObject.getValue("isDeleted");
if (deleted != null && deleted == 1) {
this.strictUpdateFill(metaObject, "deleteTime", LocalDateTime.class, LocalDateTime.now());
}
}
}
9. 替代方案对比
9.1 物理删除 vs 逻辑删除
| 特性 | 物理删除 | 逻辑删除 |
|---|---|---|
| 数据安全 | 低(不可恢复) | 高(可恢复) |
| 查询性能 | 高 | 需要额外条件 |
| 存储占用 | 低 | 会持续增长 |
| 实现复杂度 | 简单 | 需要额外处理 |
| 适用场景 | 临时数据、日志 | 业务核心数据 |
9.2 MyBatis Plus vs 手动实现
| 特性 | MyBatis Plus方案 | 手动实现方案 |
|---|---|---|
| 开发效率 | 高(自动处理) | 低(需手写SQL) |
| 灵活性 | 中等 | 高 |
| 维护成本 | 低 | 高 |
| 一致性 | 好(全局统一) | 依赖开发规范 |
| 学习曲线 | 需要了解MP特性 | 需要SQL知识 |
10. 实际项目中的经验总结
-
字段命名统一:全项目使用相同的逻辑删除字段名(如is_deleted),避免混乱
-
文档规范:在数据库设计文档中明确标注逻辑删除字段及其含义
-
新项目建议:从项目开始就规划好逻辑删除策略,避免后期改造
-
老项目改造:分阶段进行:
- 第一阶段:添加字段,不改变现有删除逻辑
- 第二阶段:逐步改造代码使用逻辑删除
- 第三阶段:完全禁用物理删除
-
团队协作:确保所有开发人员理解并正确使用逻辑删除,可以通过代码审查保证一致性
-
监控报警:对长时间存在的已删除数据设置监控,防止存储空间浪费
-
前端配合:前端需要适应逻辑删除的特性,例如:
- 列表查询默认过滤已删除项
- 提供回收站功能查看已删除项
- 重要操作增加二次确认
-
微服务场景:在分布式系统中,逻辑删除的状态变更需要通过事件通知其他服务
我在多个生产项目中实践发现,合理使用MyBatis Plus的逻辑删除功能可以显著提高数据安全性,但也要注意定期归档已删除数据,避免数据库性能下降。对于特别敏感的数据,可以结合操作日志+逻辑删除+定期备份的多重保护策略。
