一、什么是逻辑删除?
| 对比 | 物理删除 | 逻辑删除 |
|---|---|---|
| 操作 | 真正从数据库删除记录 | 仅修改一个标记字段 |
| SQL | DELETE FROM user WHERE id=1 |
UPDATE user SET deleted=1 WHERE id=1 |
| 数据 | 永久丢失 | 数据仍在,可恢复 |
| 场景 | 临时数据、日志 | 订单、用户、财务等重要数据 |
二、快速配置
第一步:数据库加字段
sql
ALTER TABLE user ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0=未删除 1=已删除';
第二步:全局配置(application.yml)
bash
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-delete-value: 1 # 删除时的值
logic-not-delete-value: 0 # 未删除时的值
第三步:实体类加注解
java
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
@TableLogic // 标记为逻辑删除字段
private Integer deleted;
}
如果已在 yml 配置了全局字段名,实体类中
@TableLogic可以省略,但加上更清晰。
三、核心原理:自动改写 SQL
配置后,MP 会自动在所有 SQL 中加上逻辑删除条件,无需手动处理。
java
// 你写的代码
userMapper.deleteById(1L);
// MP 实际执行的 SQL(不是 DELETE,而是 UPDATE)
UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0
java
// 你写的代码
userMapper.selectById(1L);
// MP 实际执行的 SQL(自动加上 deleted=0 过滤)
SELECT * FROM user WHERE id = 1 AND deleted = 0
java
// 你写的代码
userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getAge, 18));
// MP 实际执行的 SQL
SELECT * FROM user WHERE age = 18 AND deleted = 0
所有的查询、删除操作都会自动带上逻辑删除条件,完全透明!
四、各操作 SQL 对比
java
// 1. 删除
userService.removeById(1L);
// → UPDATE user SET deleted=1 WHERE id=1 AND deleted=0
// 2. 查单条
userService.getById(1L);
// → SELECT * FROM user WHERE id=1 AND deleted=0
// 3. 查列表
userService.list();
// → SELECT * FROM user WHERE deleted=0
// 4. 更新(只更新未删除的数据)
userService.updateById(user);
// → UPDATE user SET name='xx' WHERE id=1 AND deleted=0
// 5. 统计
userService.count();
// → SELECT COUNT(*) FROM user WHERE deleted=0
五、查询已删除的数据
MP 自动过滤逻辑删除数据,如果业务需要查已删除的记录,需要手动处理:
java
// 方式一:用 QueryWrapper 忽略逻辑删除(原生写法)
List<User> deletedUsers = userMapper.selectList(
new QueryWrapper<User>()
.eq("deleted", 1) // 注意:用字符串写法,不走自动过滤
);
// 方式二:在 Mapper 中自定义 SQL(推荐)
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM user WHERE deleted = 1")
List<User> selectDeletedUsers();
}
// 方式三:使用 selectMaps 绕过实体映射
List<Map<String, Object>> list = userMapper.selectMaps(
new QueryWrapper<User>().eq("deleted", 1)
);
六、真正删除(物理删除)
某些场景需要彻底删除,可以自定义 SQL:
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 真正物理删除,不受逻辑删除影响
@Delete("DELETE FROM user WHERE id = #{id}")
int physicalDeleteById(@Param("id") Long id);
// 清理已逻辑删除的数据(定期清理用)
@Delete("DELETE FROM user WHERE deleted = 1 AND deleted_time < #{expireTime}")
int cleanDeletedData(@Param("expireTime") LocalDateTime expireTime);
}
七、逻辑删除 + 唯一索引冲突问题
这是逻辑删除最常见的坑!
- 假设 username 有唯一索引
- 用户 A(username=zhangsan)被逻辑删除后,deleted=1
- 新用户想注册 username=zhangsan,插入时会报唯一键冲突!
解决方案:用联合唯一索引
sql
-- 删除原来的唯一索引
ALTER TABLE user DROP INDEX uk_username;
-- 改为联合唯一索引(username + deleted)
ALTER TABLE user ADD UNIQUE INDEX uk_username_deleted (username, deleted);
但这样还有问题,删除两次同名用户 deleted 都是 1,还是会冲突。
更好的方案:
- deleted 字段改为存 id(未删除=0,删除=自身id)
- 0 表示未删除,多个已删除记录的 deleted 值各不相同(存自己的 id)
java
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
// deleted=0 未删除,deleted=id 已删除
@TableLogic(value = "0", delval = "id")
private Long deleted;
}
sql
-- 此时联合唯一索引
ALTER TABLE user ADD UNIQUE INDEX uk_username_deleted (username, deleted);
-- 第一次删除 id=1 的 zhangsan:deleted=1,不冲突
-- 第二次删除 id=5 的 zhangsan:deleted=5,不冲突
-- 新注册 zhangsan:deleted=0,不冲突 ✅
八、完整业务示例
java
@Service
@RequiredArgsConstructor
public class UserService extends ServiceImpl<UserMapper, UserMapper> {
// 逻辑删除用户(自动执行 UPDATE SET deleted=1)
public void deleteUser(Long userId) {
boolean success = this.removeById(userId);
if (!success) throw new RuntimeException("用户不存在或已删除");
}
// 查询正常用户列表(自动过滤 deleted=1)
public List<User> listActiveUsers() {
return this.list(new LambdaQueryWrapper<User>()
.eq(User::getStatus, 1)
.orderByDesc(User::getCreateTime));
}
// 管理后台查看所有用户包括已删除(自定义 SQL)
public List<User> listAllUsers() {
return baseMapper.selectAllIncludeDeleted();
}
// 恢复已删除用户
public void restoreUser(Long userId) {
User user = new User();
user.setId(userId);
user.setDeleted(0); // 手动将 deleted 改回 0
baseMapper.updateById(user); // 注意:updateById 会加 deleted=0 条件,已删除的记录更新不到!
// 正确做法:用自定义 SQL 恢复
baseMapper.restoreById(userId);
}
}
// Mapper 中自定义 SQL
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM user")
List<User> selectAllIncludeDeleted();
@Update("UPDATE user SET deleted=0 WHERE id=#{id}")
int restoreById(@Param("id") Long id);
}
总结
逻辑删除的自动行为:
删除 → UPDATE SET deleted=1(不执行 DELETE)
查询 → 自动加 WHERE deleted=0(过滤已删除)
更新 → 自动加 WHERE deleted=0(不影响已删除记录)
需要手动处理的场景:
查已删除数据 → 自定义 SQL
物理删除 → 自定义 SQL
恢复数据 → 自定义 SQL(UPDATE SET deleted=0)
唯一索引冲突 → 联合唯一索引 + deleted 存 id
最佳实践:重要业务数据(用户、订单、商品)都应该用逻辑删除,配合定期清理任务将超过保留期的逻辑删除数据做物理清理。

逻辑删除本身也有自己的问题,比如:
- 会导致数据库表垃圾数据越来越多,影响查询效率
- SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,我不太推荐采用逻辑删除功能。
如果数据不能删除,可以采用把数据迁移到其它表的办法。