从删库到跑路?MyBatis3逻辑删除实战:优雅规避数据灾难

发布于:2025-08-02 ⋅ 阅读:(13) ⋅ 点赞:(0)

从删库到跑路?MyBatis3 逻辑删除实战:优雅规避数据灾难

在软件开发中,“删除” 操作看似简单,却暗藏玄机。误删数据导致的生产事故屡见不鲜,轻则业务中断,重则造成不可挽回的损失。逻辑删除作为一种数据保护机制,通过标记而非物理删除数据,既能满足业务 “删除” 需求,又能保留数据回溯的可能。本文将深入剖析 MyBatis3 实现逻辑删除的完整方案,从原理到实战,从基础到进阶,带你掌握这一关键技术,让数据操作更安全、更可控。

一、为什么需要逻辑删除?—— 从数据安全说起

在传统的物理删除模式中,执行DELETE语句后数据会从数据库中永久消失。这种方式看似高效,却存在诸多隐患:

  • 数据恢复困难:一旦误删,只能通过备份恢复,耗时费力且可能丢失最新数据。

  • 业务追溯断层:许多业务场景需要查询历史数据(如订单删除后仍需查看退款记录),物理删除会导致数据链条断裂。

  • 关联数据异常:删除主表数据后,从表的关联数据可能变成 “孤儿数据”,引发查询异常。

  • 合规风险:金融、医疗等行业有严格的数据留存法规,物理删除可能违反合规要求。

逻辑删除的核心思想是:不真正删除数据,而是通过一个状态字段标记数据的删除状态。当需要 “删除” 数据时,仅更新该状态字段;查询数据时,自动过滤已标记为删除的记录。这种方式既保留了数据的可恢复性,又不影响正常业务查询,成为企业级应用的标配方案。

二、逻辑删除核心原理与设计规范

2.1 核心原理拆解

逻辑删除的实现依赖三个关键环节:

  1. 状态字段设计:在数据表中添加一个用于标记删除状态的字段(如deleted)。

  2. 更新操作改造:将DELETE语句改为UPDATE语句,仅更新删除状态字段。

  3. 查询条件拦截:在所有查询语句中自动添加 “未删除” 条件,过滤已标记的数据。

以用户表为例,物理删除的 SQL 是:

DELETE FROM sys\_user WHERE id = 1;

而逻辑删除的 SQL 则变为:

UPDATE sys\_user SET deleted = 1 WHERE id = 1;

查询时自动附加条件:

SELECT \* FROM sys\_user WHERE deleted = 0;

2.2 数据库表设计规范

实现逻辑删除前,需先规范数据表设计,核心是定义删除状态字段。以下是推荐的设计标准:

字段名 类型 含义 未删除值 删除值 备注
deleted tinyint(1) 逻辑删除标记 0 1 最常用方案,占用空间小
delete_flag bit(1) 逻辑删除标记 0 1 节省存储空间,适合大数据量场景
delete_time datetime 删除时间 NULL 具体时间 可记录删除时间,兼具标记功能

deleted字段为例,创建表的 SQL 示例:

CREATE TABLE \`sys\_user\` (

  \`id\` bigint(20) NOT NULL AUTO\_INCREMENT COMMENT '用户ID',

  \`username\` varchar(50) NOT NULL COMMENT '用户名',

  \`password\` varchar(100) NOT NULL COMMENT '密码',

  \`deleted\` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记(0-未删除,1-已删除)',

  \`create\_time\` datetime NOT NULL DEFAULT CURRENT\_TIMESTAMP COMMENT '创建时间',

  PRIMARY KEY (\`id\`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

2.3 实体类设计规范

在 Java 实体类中,需对应数据库的删除状态字段,并建议添加注释说明:

import lombok.Data;

@Data

public class User {

    private Long id;

    private String username;

    private String password;

    

    /\*\*

     \* 逻辑删除标记

     \* 0-未删除,1-已删除

     \*/

    private Integer deleted;

    private LocalDateTime createTime;

}

三、MyBatis3 实现逻辑删除的三种方案

MyBatis3 作为主流的 ORM 框架,提供了多种实现逻辑删除的方式。以下将详细介绍三种常用方案,涵盖 XML 配置、注解及通用工具整合,满足不同项目场景需求。

3.1 方案一:XML 映射文件手动实现(基础方案)

这是最直接的实现方式,通过手动编写 SQL 语句实现逻辑删除,适合对 MyBatis 底层操作熟悉的开发者。

3.1.1 步骤 1:定义 Mapper 接口方法

在 Mapper 接口中定义删除和查询方法:

import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface UserMapper {

    /\*\*

     \* 逻辑删除用户

     \* @param id 用户ID

     \* @return 影响行数

     \*/

    int logicalDeleteById(@Param("id") Long id);

    

    /\*\*

     \* 查询未删除的用户列表

     \* @return 用户列表

     \*/

&#x20;   List\<User> selectNotDeletedList();

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 根据ID查询未删除的用户

&#x20;    \* @param id 用户ID

&#x20;    \* @return 用户信息

&#x20;    \*/

&#x20;   User selectNotDeletedById(@Param("id") Long id);

}
3.1.2 步骤 2:编写 XML 映射文件

在 XML 文件中编写对应的 SQL 语句,重点是将删除改为更新操作,并在查询中添加过滤条件:

\<?xml version="1.0" encoding="UTF-8"?>

\<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"&#x20;

"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

\<mapper namespace="com.example.mapper.UserMapper">

&#x20;  &#x20;

&#x20;   \<!-- 逻辑删除:更新deleted字段为1 -->

&#x20;   \<update id="logicalDeleteById">

&#x20;       UPDATE sys\_user

&#x20;       SET deleted = 1,&#x20;

&#x20;           update\_time = NOW()

&#x20;       WHERE id = #{id}

&#x20;         AND deleted = 0  \<!-- 防止重复删除 -->

&#x20;   \</update>

&#x20;  &#x20;

&#x20;   \<!-- 查询未删除用户列表:过滤deleted=0的记录 -->

&#x20;   \<select id="selectNotDeletedList" resultType="com.example.entity.User">

&#x20;       SELECT id, username, password, deleted, create\_time

&#x20;       FROM sys\_user

&#x20;       WHERE deleted = 0

&#x20;       ORDER BY create\_time DESC

&#x20;   \</select>

&#x20;  &#x20;

&#x20;   \<!-- 根据ID查询未删除用户 -->

&#x20;   \<select id="selectNotDeletedById" resultType="com.example.entity.User">

&#x20;       SELECT id, username, password, deleted, create\_time

&#x20;       FROM sys\_user

&#x20;       WHERE id = #{id}

&#x20;         AND deleted = 0  \<!-- 仅查询未删除记录 -->

&#x20;   \</select>

\</mapper>
3.1.3 步骤 3:Service 层调用

在 Service 中调用 Mapper 方法,实现业务逻辑:

import org.springframework.stereotype.Service;

import javax.annotation.Resource;

import java.util.List;

@Service

public class UserService {

&#x20;   @Resource

&#x20;   private UserMapper userMapper;

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 逻辑删除用户

&#x20;    \*/

&#x20;   public boolean deleteUser(Long id) {

&#x20;       if (id == null) {

&#x20;           throw new IllegalArgumentException("用户ID不能为空");

&#x20;       }

&#x20;       // 先查询用户是否存在且未删除

&#x20;       User user = userMapper.selectNotDeletedById(id);

&#x20;       if (user == null) {

&#x20;           throw new RuntimeException("用户不存在或已删除");

&#x20;       }

&#x20;       // 执行逻辑删除

&#x20;       int rows = userMapper.logicalDeleteById(id);

&#x20;       return rows > 0;

&#x20;   }

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 获取未删除用户列表

&#x20;    \*/

&#x20;   public List\<User> getUserList() {

&#x20;       return userMapper.selectNotDeletedList();

&#x20;   }

}
3.1.4 方案优缺点分析
  • 优点:实现简单直接,SQL 语句完全可控,适合复杂业务场景。

  • 缺点:需手动编写所有 SQL,重复劳动多,易遗漏查询条件导致数据泄露。

3.2 方案二:注解方式实现(简化方案)

MyBatis 支持通过注解编写 SQL,对于简单的逻辑删除场景,可以采用注解方式减少 XML 配置。

3.2.1 步骤 1:定义 Mapper 接口(注解版)

直接在接口方法上使用@Update@Select注解编写 SQL:

import org.apache.ibatis.annotations.Delete;

import org.apache.ibatis.annotations.Select;

import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface UserAnnotationMapper {

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 逻辑删除用户(注解版)

&#x20;    \*/

&#x20;   @Update("UPDATE sys\_user SET deleted = 1, update\_time = NOW() WHERE id = #{id} AND deleted = 0")

&#x20;   int logicalDeleteById(Long id);

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 查询未删除用户(注解版)

&#x20;    \*/

&#x20;   @Select("SELECT id, username, password, deleted, create\_time FROM sys\_user WHERE deleted = 0")

&#x20;   List\<User> selectNotDeletedList();

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 根据ID查询未删除用户(注解版)

&#x20;    \*/

&#x20;   @Select("SELECT id, username, password, deleted, create\_time FROM sys\_user WHERE id = #{id} AND deleted = 0")

&#x20;   User selectNotDeletedById(Long id);

}
3.2.2 步骤 2:Service 层调用(与 XML 方案一致)
@Service

public class UserAnnotationService {

&#x20;   @Resource

&#x20;   private UserAnnotationMapper userAnnotationMapper;

&#x20;  &#x20;

&#x20;   public boolean deleteUser(Long id) {

&#x20;       // 逻辑与XML方案相同

&#x20;       User user = userAnnotationMapper.selectNotDeletedById(id);

&#x20;       if (user == null) {

&#x20;           throw new RuntimeException("用户不存在或已删除");

&#x20;       }

&#x20;       return userAnnotationMapper.logicalDeleteById(id) > 0;

&#x20;   }

}
3.2.3 方案优缺点分析
  • 优点:无需编写 XML 文件,代码更集中,适合简单 SQL 场景。

  • 缺点:复杂 SQL 在注解中可读性差,同样存在重复编写条件的问题。

3.3 方案三:通用 Mapper 整合(企业级方案)

对于中大型项目,推荐使用通用 Mapper(如 MyBatis-Plus)实现逻辑删除,通过全局配置和拦截器自动处理删除标记,减少重复代码。

3.3.1 步骤 1:引入 MyBatis-Plus 依赖

pom.xml中添加依赖(以 Spring Boot 为例):

\<!-- MyBatis-Plus核心依赖 -->

\<dependency>

&#x20;   \<groupId>com.baomidou\</groupId>

&#x20;   \<artifactId>mybatis-plus-boot-starter\</artifactId>

&#x20;   \<version>3.5.3.1\</version>

\</dependency>

\<!-- 数据库驱动 -->

\<dependency>

&#x20;   \<groupId>com.mysql\</groupId>

&#x20;   \<artifactId>mysql-connector-j\</artifactId>

&#x20;   \<scope>runtime\</scope>

\</dependency>
3.3.2 步骤 2:配置逻辑删除(application.yml)

通过配置文件全局定义逻辑删除的字段名和值:

mybatis-plus:

&#x20; global-config:

&#x20;   db-config:

&#x20;     \# 逻辑删除字段名

&#x20;     logic-delete-field: deleted

&#x20;     \# 逻辑未删除值(默认为0)

&#x20;     logic-not-delete-value: 0

&#x20;     \# 逻辑已删除值(默认为1)

&#x20;     logic-delete-value: 1

&#x20; configuration:

&#x20;   \# 开启日志,方便调试

&#x20;   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3.3.3 步骤 3:定义实体类(添加注解)

在实体类的删除字段上添加@TableLogic注解,标记为逻辑删除字段:

import com.baomidou.mybatisplus.annotation.\*;

import lombok.Data;

import java.time.LocalDateTime;

@Data

@TableName("sys\_user")

public class User {

&#x20;   @TableId(type = IdType.AUTO)

&#x20;   private Long id;

&#x20;   private String username;

&#x20;   private String password;

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 逻辑删除标记

&#x20;    \* 0-未删除,1-已删除

&#x20;    \*/

&#x20;   @TableLogic

&#x20;   private Integer deleted;

&#x20;  &#x20;

&#x20;   // 自动填充创建时间

&#x20;   @TableField(fill = FieldFill.INSERT)

&#x20;   private LocalDateTime createTime;

&#x20;  &#x20;

&#x20;   // 自动填充更新时间

&#x20;   @TableField(fill = FieldFill.INSERT\_UPDATE)

&#x20;   private LocalDateTime updateTime;

}
3.3.4 步骤 4:定义 Mapper 接口(继承 BaseMapper)

无需手动编写删除和查询方法,直接继承 MyBatis-Plus 的BaseMapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.example.entity.User;

public interface UserPlusMapper extends BaseMapper\<User> {

&#x20;   // 无需编写额外方法,BaseMapper已提供CRUD操作

}
3.3.5 步骤 5:Service 层调用(使用内置方法)

MyBatis-Plus 的IService接口提供了丰富的内置方法,自动支持逻辑删除:

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.example.entity.User;

import com.example.mapper.UserPlusMapper;

import org.springframework.stereotype.Service;

import java.util.List;

@Service

public class UserPlusService extends ServiceImpl\<UserPlusMapper, User> {

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 逻辑删除用户(直接调用内置方法)

&#x20;    \*/

&#x20;   public boolean deleteUser(Long id) {

&#x20;       // removeById方法会自动执行逻辑删除

&#x20;       return removeById(id);

&#x20;   }

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 查询未删除用户列表(自动过滤已删除记录)

&#x20;    \*/

&#x20;   public List\<User> getUserList() {

&#x20;       // list方法会自动添加deleted=0条件

&#x20;       return list();

&#x20;   }

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 根据ID查询用户(自动过滤已删除记录)

&#x20;    \*/

&#x20;   public User getUserById(Long id) {

&#x20;       // getById方法会自动添加deleted=0条件

&#x20;       return getById(id);

&#x20;   }

}
3.3.6 原理揭秘:MyBatis-Plus 的逻辑删除拦截器

MyBatis-Plus 通过LogicDeleteInterceptor拦截器实现逻辑删除的自动处理:

  1. 删除拦截:将delete方法拦截,转换为update语句更新deleted字段。

  2. 查询拦截:在select语句后自动添加WHERE deleted = 0条件。

  3. 更新拦截:在update语句中自动添加WHERE deleted = 0条件,防止更新已删除记录。

通过日志可以看到实际执行的 SQL:

\-- 调用removeById(1)时执行的SQL

UPDATE sys\_user SET deleted=1 WHERE id=1 AND deleted=0

\-- 调用list()时执行的SQL

SELECT id,username,password,deleted,create\_time,update\_time FROM sys\_user WHERE deleted=0

\-- 调用getById(1)时执行的SQL

SELECT id,username,password,deleted,create\_time,update\_time FROM sys\_user WHERE id=1 AND deleted=0
3.3.7 方案优缺点分析
  • 优点:全局配置,自动处理所有 CRUD 操作,无需手动编写 SQL,减少重复劳动和出错概率。

  • 缺点:需要学习 MyBatis-Plus 的使用规范,对原生 MyBatis 有一定封装,自定义 SQL 时需注意兼容逻辑删除。

四、实战案例:完整实现一个区域管理系统的逻辑删除

以下将通过一个区域管理系统的实战案例,完整展示 MyBatis-Plus 实现逻辑删除的全过程,包括数据库设计、代码实现、接口测试等环节。

4.1 需求分析

实现一个区域管理系统,支持区域的新增、查询、更新和删除操作,其中删除操作需采用逻辑删除,并支持级联删除子区域。

4.2 数据库设计

创建区域表region,包含层级关系和逻辑删除字段:

CREATE TABLE \`region\` (

&#x20; \`id\` bigint(20) NOT NULL AUTO\_INCREMENT COMMENT '区域ID',

&#x20; \`parent\_id\` bigint(20) DEFAULT 0 COMMENT '父区域ID(0表示顶级区域)',

&#x20; \`region\_name\` varchar(100) NOT NULL COMMENT '区域名称',

&#x20; \`level\` tinyint(1) NOT NULL COMMENT '层级(1:一级,2:二级...)',

&#x20; \`deleted\` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记(0-未删除,1-已删除)',

&#x20; \`create\_time\` datetime NOT NULL DEFAULT CURRENT\_TIMESTAMP COMMENT '创建时间',

&#x20; \`update\_time\` datetime DEFAULT NULL ON UPDATE CURRENT\_TIMESTAMP COMMENT '更新时间',

&#x20; PRIMARY KEY (\`id\`),

&#x20; KEY \`idx\_parent\_id\` (\`parent\_id\`),

&#x20; KEY \`idx\_deleted\` (\`deleted\`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='区域表';

4.3 项目结构

com.example.region

├── RegionApplication.java       // 启动类

├── config                       // 配置类

│   └── MyBatisPlusConfig.java   // MyBatis-Plus配置

├── entity                       // 实体类

│   └── Region.java              // 区域实体

├── mapper                       // Mapper接口

│   └── RegionMapper.java        // 区域Mapper

├── service                      // Service层

│   ├── RegionService.java       // 区域Service接口

│   └── impl

│       └── RegionServiceImpl.java // Service实现

└── controller                   // Controller层

&#x20;   └── RegionController.java    // 区域控制器

4.4 核心代码实现

4.4.1 实体类 Region.java
import com.baomidou.mybatisplus.annotation.\*;

import io.swagger.v3.oas.annotations.media.Schema;

import lombok.Data;

import java.time.LocalDateTime;

@Data

@TableName("region")

@Schema(description = "区域实体类")

public class Region {

&#x20;   @TableId(type = IdType.AUTO)

&#x20;   @Schema(description = "区域ID")

&#x20;   private Long id;

&#x20;  &#x20;

&#x20;   @Schema(description = "父区域ID(0表示顶级区域)")

&#x20;   private Long parentId;

&#x20;  &#x20;

&#x20;   @Schema(description = "区域名称")

&#x20;   private String regionName;

&#x20;  &#x20;

&#x20;   @Schema(description = "层级(1:一级,2:二级...)")

&#x20;   private Integer level;

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 逻辑删除标记

&#x20;    \* 0-未删除,1-已删除

&#x20;    \*/

&#x20;   @TableLogic

&#x20;   @Schema(description = "逻辑删除标记", hidden = true)

&#x20;   private Integer deleted;

&#x20;  &#x20;

&#x20;   @TableField(fill = FieldFill.INSERT)

&#x20;   @Schema(description = "创建时间", hidden = true)

&#x20;   private LocalDateTime createTime;

&#x20;  &#x20;

&#x20;   @TableField(fill = FieldFill.INSERT\_UPDATE)

&#x20;   @Schema(description = "更新时间", hidden = true)

&#x20;   private LocalDateTime updateTime;

}
4.4.2 Mapper 接口 RegionMapper.java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.example.entity.Region;

import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface RegionMapper extends BaseMapper\<Region> {

&#x20;   /\*\*

&#x20;    \* 根据父ID查询子区域(MyBatis-Plus会自动添加deleted=0条件)

&#x20;    \*/

&#x20;   @Select("SELECT \* FROM region WHERE parent\_id = #{parentId}")

&#x20;   List\<Region> selectChildrenByParentId(Long parentId);

}
4.4.3 Service 接口与实现
// RegionService.java

import com.baomidou.mybatisplus.extension.service.IService;

import com.example.entity.Region;

import java.util.List;

public interface RegionService extends IService\<Region> {

&#x20;   /\*\*

&#x20;    \* 级联逻辑删除区域(含子区域)

&#x20;    \* @param id 区域ID

&#x20;    \* @return 是否删除成功

&#x20;    \*/

&#x20;   boolean cascadeDelete(Long id);

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 查询区域树形结构

&#x20;    \* @return 区域树列表

&#x20;    \*/

&#x20;   List\<Region> getRegionTree();

}

// RegionServiceImpl.java

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.example.entity.Region;

import com.example.mapper.RegionMapper;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service

public class RegionServiceImpl extends ServiceImpl\<RegionMapper, Region> implements RegionService {

&#x20;   @Override

&#x20;   @Transactional(rollbackFor = Exception.class)

&#x20;   public boolean cascadeDelete(Long id) {

&#x20;       // 1. 查询当前区域是否存在

&#x20;       Region region = getById(id);

&#x20;       if (region == null) {

&#x20;           return false;

&#x20;       }

&#x20;      &#x20;

&#x20;       // 2. 递归删除所有子区域

&#x20;       deleteChildren(id);

&#x20;      &#x20;

&#x20;       // 3. 删除当前区域(逻辑删除)

&#x20;       return removeById(id);

&#x20;   }

&#x20;  &#x20;

&#x20;   /\*\*

&#x20;    \* 递归删除子区域

&#x20;    \*/

&#x20;   private void deleteChildren(Long parentId) {

&#x20;       List\<Region> children = baseMapper.selectChildrenByParentId(parentId);

&#x20;       if (children != null && !children.isEmpty()) {

&#x20;           for (Region child : children) {

&#x20;               // 递归删除子区域的子节点

&#x20;               deleteChildren(child.getId());

&#x20;               // 删除当前子区域

&#x20;               removeById(child.getId());

&#x20;           }

&#x20;       }

&#x20;   }

&#x20;  &#x20;

&#x20;   @Override

&#x20;   public List\<Region> getRegionTree() {

&#x20;       // 查询所有顶级区域(parent\_id=0)

&#x20;       QueryWrapper\<Region> queryWrapper = new QueryWrapper<>();

&#x20;       queryWrapper.eq("parent\_id", 0);

&#x20;       return baseMapper.selectList(queryWrapper);

&#x20;   }

}
4.4.4 Controller 层实现(带 Swagger 文档)
import com.example.entity.Region;

import com.example.service.RegionService;

import io.swagger.v3.oas.annotations.Operation;

import io.swagger.v3.oas.annotations.Parameter;

import io.swagger.v3.oas.annotations.tags.Tag;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.\*;

import java.util.List;

@RestController

@RequestMapping("/api/regions")

@Tag(name = "区域管理", description = "区域CRUD接口(含逻辑删除)")

public class RegionController {

&#x20;   private final RegionService regionService;

&#x20;  &#x20;

&#x20;   // 构造方法注入

&#x20;   public RegionController(RegionService regionService) {

&#x20;       this.regionService = regionService;

&#x20;   }

&#x20;  &#x20;

&#x20;   @PostMapping

&#x20;   @Operation(summary = "新增区域")

&#x20;   public ResponseEntity\<Boolean> addRegion(@RequestBody Region region) {

&#x20;       return ResponseEntity.ok(regionService.save(region));

&#x20;   }

&#x20;  &#x20;

&#x20;   @GetMapping

&#x20;   @Operation(summary = "查询所有未删除区域")

&#x20;   public ResponseEntity\<List\<Region>> getAllRegions() {

&#x20;       return ResponseEntity.ok(regionService.list());

&#x20;   }

&#x20;  &#x20;

&#x20;   @GetMapping("/{id}")

&#x20;   @Operation(summary = "根据ID查询区域")

&#x20;   public ResponseEntity\<Region> getRegionById(

&#x20;           @Parameter(description = "区域ID", required = true)

&#x20;           @PathVariable Long id) {

&#x20;       return ResponseEntity.ok(regionService.getById(id));

&#x20;   }

&#x20;  &#x20;

&#x20;   @DeleteMapping("/{id}")

&#x20;   @Operation(summary = "级联逻辑删除区域")

&#x20;   public ResponseEntity\<Boolean> deleteRegion(

&#x20;           @Parameter(description = "区域ID", required = true)

&#x20;           @PathVariable Long id) {

&#x20;       return ResponseEntity.ok(regionService.cascadeDelete(id));

&#x20;   }

&#x20;  &#x20;

&#x20;   @GetMapping("/tree")

&#x20;   @Operation(summary = "查询区域树形结构")

&#x20;   public ResponseEntity\<List\<Region>> getRegionTree() {

&#x20;       return ResponseEntity.ok(regionService.getRegionTree());

&#x20;   }

}

4.5 测试验证

通过 Postman 或 Swagger 文档测试接口,验证逻辑删除效果:

  1. 新增区域:发送 POST 请求/api/regions,添加顶级区域和子区域。

  2. 查询区域:发送 GET 请求/api/regions,返回所有未删除区域。

  3. 删除区域:发送 DELETE 请求/api/regions/{id},执行逻辑删除。

  4. 验证删除:再次查询该区域,返回结果为 null;查询数据库,deleted字段变为 1。

  5. 级联删除测试:删除顶级区域,检查其所有子区域的deleted字段是否均变为 1。

五、进阶技巧:让逻辑删除更高效、更安全

5.1 级联逻辑删除的实现方案

在树形结构(如区域、菜单)中,删除父节点时需级联删除所有子节点。除了上述案例中的递归删除,还可采用以下优化方案:

5.1.1 方案一:使用数据库存储过程

通过存储过程批量更新子节点,减少 Java 代码中的递归调用:

\-- 创建级联逻辑删除存储过程

DELIMITER \$\$

CREATE PROCEDURE \`cascade\_logic\_delete\_region\`(IN parentId BIGINT)

BEGIN

&#x20;   \-- 声明变量

&#x20;   DECLARE done INT DEFAULT 0;

&#x20;   DECLARE childId BIGINT;

&#x20;   \-- 定义游标

&#x20;   DECLARE childCursor CURSOR FOR&#x20;

&#x20;       SELECT id FROM region WHERE parent\_id = parentId AND deleted = 0;

&#x20;   \-- 定义异常处理

&#x20;   DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

&#x20;  &#x20;

&#x20;   \-- 递归处理子节点

&#x20;   OPEN childCursor;

&#x20;   read\_loop: LOOP

&#x20;       FETCH childCursor INTO childId;

&#x20;       IF done THEN

&#x20;           LEAVE read\_loop;

&#x20;       END IF;

&#x20;       \-- 递归调用存储过程处理子节点的子节点

&#x20;       CALL cascade\_logic\_delete\_region(childId);

&#x20;   END LOOP;

&#x20;   CLOSE childCursor;

&#x20;  &#x20;

&#x20;   \-- 更新当前节点的删除状态

&#x20;   UPDATE region SET deleted = 1 WHERE id = parentId;

END\$\$

DELIMITER ;

\-- 调用存储过程删除ID=1的区域及其子区域

CALL cascade\_logic\_delete\_region(1);

在 MyBatis 中调用存储过程:

\<update id="cascadeDeleteByProcedure">

&#x20;   CALL cascade\_logic\_delete\_region(#{id})

\</update>
5.1.2 方案二:使用 MyBatis-Plus 的批量更新

通过updateBatchById方法批量更新子节点,减少 SQL 执行次数:

@Override

@Transactional(rollbackFor = Exception.class)

public boolean cascadeDelete(Long id) {

&#x20;   // 1. 查询所有子节点ID(递归查询)

&#x20;   List\<Long> allChildIds = getAllChildIds(id);

&#x20;   allChildIds.add(id); // 包含当前节点

&#x20;  &#x20;

&#x20;   // 2. 批量更新删除状态

&#x20;   List\<Region> updateList = allChildIds.stream().map(childId -> {

&#x20;       Region region = new Region();

&#x20;       region.setId(childId);

&#x20;       region.setDeleted(1); // 逻辑删除值

&#x20;       return region;

&#x20;   }).collect(Collectors.toList());

&#x20;  &#x20;

&#x20;   // 3. 批量更新

&#x20;   return updateBatchById(updateList);

}

/\*\*

&#x20;\* 递归查询所有子节点ID

&#x20;\*/

private List\<Long> getAllChildIds(Long parentId) {

&#x20;   List\<Region> children = baseMapper.selectChildrenByParentId(parentId);

&#x20;   if (children.isEmpty()) {

&#x20;       return new ArrayList<>();

&#x20;   }

&#x20;   List\<Long> childIds = new ArrayList<>();

&#x20;   for (Region child : children) {

&#x20;       childIds.add(child.getId());

&#x20;       // 递归添加子节点的子节点

&#x20;       childIds.addAll(getAllChildIds(child.getId()));

&#x20;   }

&#x20;   return childIds;

}

5.2 逻辑删除与索引优化

逻辑删除会导致表中存在大量 “已删除” 数据,影响查询性能。需通过索引优化提升查询效率:

  1. 创建联合索引:将deleted字段与常用查询条件创建联合索引,如:
\-- 为区域表创建parent\_id+deleted联合索引

CREATE INDEX idx\_parent\_deleted ON region(parent\_id, deleted);
  1. 避免全表扫描:确保查询条件中包含deleted字段,MyBatis-Plus 的内置方法已自动处理,但自定义 SQL 需注意:
\<!-- 错误示例:未包含deleted条件,可能导致全表扫描 -->

\<select id="selectByParentId" resultType="Region">

&#x20;   SELECT \* FROM region WHERE parent\_id = #{parentId}

\</select>

\<!-- 正确示例:包含deleted条件,使用联合索引 -->

\<select id="selectByParentId" resultType="Region">

&#x20;   SELECT \* FROM region WHERE parent\_id = #{parentId} AND deleted = 0

\</select>
  1. 定期归档清理:对于数据量极大的表,可定期将已删除数据归档到历史表,减少主表数据量:
\-- 创建历史表

CREATE TABLE \`region\_history\` LIKE \`region\`;

\-- 归档已删除数据

INSERT INTO region\_history&#x20;

SELECT \* FROM region WHERE deleted = 1 AND update\_time < DATE\_SUB(NOW(), INTERVAL 1 YEAR);

\-- 删除主表中已归档的数据(物理删除)

DELETE FROM region WHERE deleted = 1 AND update\_time < DATE\_SUB(NOW(), INTERVAL 1 YEAR);

5.3 逻辑删除与数据审计

结合审计日志记录删除操作,便于追踪和回溯:

  1. 添加审计字段:在表中增加删除人、删除时间字段:
ALTER TABLE region&#x20;

ADD COLUMN \`delete\_by\` bigint(20) DEFAULT NULL COMMENT '删除人',

ADD COLUMN \`delete\_time\` datetime DEFAULT NULL COMMENT '删除时间';
  1. 实体类添加对应字段
@TableField(fill = FieldFill.UPDATE) // 删除时自动填充

private Long deleteBy;

@TableField(fill = FieldFill.UPDATE)

private LocalDateTime deleteTime;
  1. 实现自动填充处理器
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;

import org.apache.ibatis.reflection.MetaObject;

import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component

public class MyMetaObjectHandler implements MetaObjectHandler {

&#x20;   @Override

&#x20;   public void insertFill(MetaObject metaObject) {

&#x20;       // 自动填充创建时间

&#x20;       this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());

&#x20;   }

&#x20;  &#x20;

&#x20;   @Override

&#x20;   public void updateFill(MetaObject metaObject) {

&#x20;       // 自动填充更新时间

&#x20;       this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());

&#x20;      &#x20;

&#x20;       // 判断是否为逻辑删除操作(deleted字段被更新为1)

&#x20;       Object deleted = metaObject.getValue("deleted");

&#x20;       if (deleted != null && deleted.equals(1)) {

&#x20;           // 填充删除人(实际应从当前登录用户获取)

&#x20;           this.strictUpdateFill(metaObject, "deleteBy", Long.class, 1L);

&#x20;           // 填充删除时间

&#x20;           this.strictUpdateFill(metaObject, "deleteTime", LocalDateTime.class, LocalDateTime.now());

&#x20;       }

&#x20;   }

}

5.4 逻辑删除的数据恢复方案

当误删数据时,可通过以下方式恢复:

  1. 单条数据恢复:直接更新deleted字段为 0:
/\*\*

&#x20;\* 恢复逻辑删除的数据

&#x20;\*/

public boolean recover(Long id) {

&#x20;   Region region = new Region();

&#x20;   region.setId(id);

&#x20;   region.setDeleted(0); // 恢复为未删除状态

&#x20;   return updateById(region);

}
  1. 批量恢复子区域:类似级联删除,递归恢复子区域:
@Transactional(rollbackFor = Exception.class)

public boolean cascadeRecover(Long id) {

&#x20;   // 恢复当前区域

&#x20;   Region region = new Region();

&#x20;   region.setId(id);

&#x20;   region.setDeleted(0);

&#x20;   updateById(region);

&#x20;  &#x20;

&#x20;   // 递归恢复子区域

&#x20;   List\<Region> children = baseMapper.selectChildrenByParentId(id);

&#x20;   for (Region child : children) {

&#x20;       cascadeRecover(child.getId());

&#x20;   }

&#x20;   return true;

}

六、常见问题与避坑指南

6.1 问题 1:查询时仍能查到已删除数据

可能原因

  • 自定义 SQL 未添加deleted = 0条件。

  • MyBatis-Plus 的拦截器未生效(如配置错误)。

  • 实体类未添加@TableLogic注解。

解决方案

  • 检查自定义 SQL,确保包含删除条件。

  • 验证 MyBatis-Plus 配置,确认logic-delete-field正确。

  • 在删除字段上添加@TableLogic注解。

6.2 问题 2:级联删除时子区域未被删除

可能原因

  • 递归逻辑错误,未正确获取所有子节点。

  • 事务未生效,部分删除操作失败后未回滚。

  • 子区域查询 SQL 未过滤已删除节点,导致重复处理。

解决方案

  • 调试递归方法,确保所有子节点 ID 被正确收集。

  • 添加@Transactional注解,确保事务完整性。

  • 查询子区域时添加deleted = 0条件(MyBatis-Plus 自动处理)。

6.3 问题 3:逻辑删除与唯一索引冲突

场景:表中存在唯一索引(如username),逻辑删除后无法添加同名用户。

原因:逻辑删除的记录仍存在于表中,唯一索引会阻止重复值插入。

解决方案

  • deleted字段加入唯一索引,如:
\-- 创建包含deleted的唯一索引

CREATE UNIQUE INDEX uk\_username\_deleted ON sys\_user(username, deleted);
  • 这样,username相同但deleted不同的记录可以共存(未删除记录的deleted=0,已删除的deleted=1)。

6.4 问题 4:性能下降,查询变慢

可能原因

  • 表中已删除数据过多,导致索引失效。

  • 未创建合适的联合索引,查询走全表扫描。

  • 递归查询子区域时产生过多 SQL 调用。

解决方案

  • 定期归档已删除数据,减少主表数据量。

  • 优化索引设计,添加deleted字段到常用索引。

  • 使用批量查询替代递归查询,减少数据库交互。

6.5 问题 5:数据迁移时逻辑删除字段处理

场景:迁移数据到新库时,需保留逻辑删除状态。

解决方案

  • 迁移脚本中明确处理deleted字段,避免默认值覆盖。

  • 迁移后验证数据,确保未删除记录的deleted值正确。

  • 示例迁移 SQL:

\-- 从旧表迁移数据到新表,保留deleted状态

INSERT INTO new\_region (id, parent\_id, region\_name, level, deleted)

SELECT id, parent\_id, region\_name, level, deleted FROM old\_region;

七、总结与展望

逻辑删除作为保障数据安全的重要手段,在企业级应用中不可或缺。本文从原理到实战,详细讲解了 MyBatis3 实现逻辑删除的三种方案,通过完整案例展示了逻辑删除的核心流程,并提供了级联删除、性能优化、数据恢复等进阶技巧。

随着业务复杂度的提升,逻辑删除还可与以下技术结合:

  • 数据权限控制:在逻辑删除基础上,结合用户权限过滤数据。

  • 软删除与硬删除结合:对超期数据执行物理删除,平衡性能与安全。

  • 分布式事务:在微服务架构中,通过 Seata 等框架保证跨服务逻辑删除的一致性。

掌握逻辑删除不仅是技术需求,更是数据安全意识的体现。希望本文能帮助你在实际项目中优雅地实现逻辑删除,规避数据风险,让系统更健壮、更可靠。

最后,记住一句话:在数据领域,“删除” 永远应该是可逆的操作,除非你 100% 确定再也不需要这些数据。逻辑删除,正是这种理念的最佳实践。

(注:文档部分内容可能由 AI 生成)


网站公告

今日签到

点亮在社区的每一天
去签到