MyBatis-Plus10:逻辑删除

一、什么是逻辑删除?

对比 物理删除 逻辑删除
操作 真正从数据库删除记录 仅修改一个标记字段
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中全都需要对逻辑删除字段做判断,影响查询效率

因此,我不太推荐采用逻辑删除功能。

如果数据不能删除,可以采用把数据迁移到其它表的办法。

相关推荐
树码小子1 天前
Mybatis(16)Mybatis-Plus条件构造器(1)
数据库·mybatis-plus
树码小子2 天前
Mybatis(14)Mybatis-Plus入门 & 简单使用
java·mybatis-plus
ruleslol3 天前
MyBatis-Plus06:IService接口Lambda基本用法
mybatis-plus
ruleslol4 天前
MyBatis-Plus02: 常用注解
mybatis-plus
ruleslol6 天前
MyBatis-Plus05:IService接口基本用法
mybatis-plus
ruleslol6 天前
MyBatis-Plus04:自定义SQL
mybatis-plus
识君啊10 天前
MyBatis-Plus 逻辑删除导致唯一索引冲突的解决方案
java·spring boot·mybatis·mybatis-plus·唯一索引·逻辑删除
独断万古他化18 天前
【MyBatis-Plus 进阶】注解配置、条件构造器与自定义 SQL的复杂操作详解
sql·mybatis·mybatis-plus·条件构造器
子沫20201 个月前
使用mybatis-plus、mybatis插入数据库时加密,查询数据库时解密,自定义TypeHandler 加解密使用
数据库·mybatis·mybatis-plus