当前位置: 首页 > news >正文

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

从删库到跑路?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 数据库表设计规范

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

字段名类型含义未删除值删除值备注
deletedtinyint(1)逻辑删除标记01最常用方案,占用空间小
delete_flagbit(1)逻辑删除标记01节省存储空间,适合大数据量场景
delete_timedatetime删除时间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;@Datapublic 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 {&#x20;   /\*\*&#x20;    \* 逻辑删除用户&#x20;    \* @param id 用户ID&#x20;    \* @return 影响行数&#x20;    \*/&#x20;   int logicalDeleteById(@Param("id") Long id);&#x20;  &#x20;&#x20;   /\*\*&#x20;    \* 查询未删除的用户列表&#x20;    \* @return 用户列表&#x20;    \*/&#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;@Servicepublic 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 方案一致)
@Servicepublic 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;@Servicepublic 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)时执行的SQLUPDATE sys\_user SET deleted=1 WHERE id=1 AND deleted=0\-- 调用list()时执行的SQLSELECT id,username,password,deleted,create\_time,update\_time FROM sys\_user WHERE deleted=0\-- 调用getById(1)时执行的SQLSELECT 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.javaimport 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.javaimport 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;@Servicepublic 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;@Componentpublic 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 生成)

http://www.dtcms.com/a/310805.html

相关文章:

  • 一致连续性背后的直觉是什么?
  • 高速信号设计之 DDR5 篇
  • 【unity实战】简易的车辆控制系统
  • 从零开始:Kaggle 竞赛实战入门指南
  • 鸿蒙系统PC安装指南
  • 【RH124 问答题】第 9 章 控制服务和守护进程
  • 测试分类:详解各类测试方式与方法
  • 告别“AI味”图像!最新开源AI模型FLUX.1-Krea实现真实光影生成
  • 【n8n】如何跟着AI学习n8n【05】:Merge节点和子流程调用
  • Prim算法
  • 交叉编译简介
  • 【JAVA面试】基础篇
  • 广东省省考备考(第六十三天8.1)——资料分析、数量(强化训练)
  • 【AI应用】 能源保供战:AI大模型如何守护万家灯火?
  • Day37| 完全背包、518.零钱兑换II、377. 组合总和 Ⅳ、70. 爬楼梯 (进阶)
  • 流式编程学习思路
  • 疯狂星期四文案网第26天运营日记
  • 【PyTorch✨】01 初识PyTorch
  • 潜伏式 AGV 与叉车 AGV 充电桩的技术差异及应用分析
  • 在国内注册谷歌邮箱(资源是免费下载的)
  • 第13届蓝桥杯Python青少组中/高级组选拔赛(STEMA)2021年11月27日真题
  • Linux文件系统:从内核到缓冲区的奥秘
  • PyTorch深度学习入门记录8
  • 逻辑回归参数调优实战指南
  • MeshDepthMaterial
  • AI论文工具的应用与发展(2025年总结)
  • SQL数据库连接Python实战:疫情数据指挥中心搭建指南
  • 嵌入式学习之硬件——51单片机 1.0
  • QPS 与 TPS 的详细解释及核心区别
  • DLL错误专修工具_TBI3264.exe下载安装教程(一键修复DLL缺失/错误)​