作者 lucy · 2026-04-11
⚠️ 声明:本文相关内容仅供参考,实际效果因场景不同可能差异很大,请结合自身情况判断,谨慎参考。
一次 MyBatis-Plus 逻辑删除踩坑实录:数据”消失”了却找不到原因
做后端开发这么多年,最让人崩溃的 bug 往往不是逻辑写错了,而是系统行为和预期不符,却找不到代码哪里动了手脚。今天分享一个我在实际项目中遇到的真实案例——数据明明存在,查询结果却是空的,追查半天才发现是框架的”好意”在作怪。
🐢 问题现场
项目里有个订单模块,用的是 MyBatis-Plus + RuoYi 框架二次开发。某天运营反馈:一个订单的数据在数据库里明明存在,但在订单详情页死活查不出来。更诡异的是,关联的订单商品列表倒是能查到,就是主表订单查不到。
直觉告诉我可能是 ID 传错了或者状态字段有问题。查了一圈日志,SQL 打印出来是这样的:
SELECT * FROM order_main WHERE id = 'ORD20260318001' LIMIT 1
执行计划显示走索引了,返回 0 条记录。但我去数据库直接查:
mysql> SELECT * FROM order_main WHERE id = 'ORD20260318001';
+------+------------+--------+-------------+------+
| id | order_no | status | deleted_at | ... |
+------+------------+--------+-------------+------+
| 1 | ORD20260318001 | 3 | 2026-03-17 | ... |
+------+------------+--------+-------------+------+
数据就在那里安安静静躺着,deleted_at 字段有值(表示被删除了)。但代码里没有写任何删除逻辑,这个字段怎么会被填上的?
🔍 根因定位
翻遍 Service 层代码,没有任何手动删除操作。逐一检查 Controller、Service、Mapper,最后目光落在实体类上:
@Data
@TableName("order_main")
public class OrderMain {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String orderNo;
private Integer status;
// 其他字段省略...
}
没看到逻辑删除注解啊?但我顺手查了一下父类基类——
@Data
public class BaseEntity {
@TableField(fill = FieldFill.INSERT)
private Long createBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
还是没有逻辑删除字段。再检查 Mapper 文件:
@Mapper
public interface OrderMainMapper extends BaseMapper<OrderMain> {
}
没有问题。等等——我记得 MyBatis-Plus 3.3.0 之后有全局逻辑删除配置,难道有人改过?翻配置文件:
# application.yml
mybatis-plus:
global-config:
db-config:
# 逻辑删除配置
logic-delete-field: deletedAt
logic-delete-value: "1"
logic-not-delete-value: "0"
找到了!项目全局配置了逻辑删除字段为 deletedAt,但实体类里根本没有声明这个字段。
⚠️ 问题的本质
这才是坑的真正原因:
- 全局配置了逻辑删除字段
deletedAt,MP 在运行时自动给所有 SELECT 查询加上WHERE deletedAt = 0条件。 - 实体类没有声明
deletedAt字段,所以 MP 用了字段映射规则,把数据库的deleted_at映射到deletedAt属性。 - 一旦数据被软删除(
deleted_at被填上时间戳),MP 的查询拦截器就会把它排除在外。
那数据是怎么被”删除”的呢?原来月初做数据迁移时,DBA 批量更新了一批历史脏数据:
UPDATE order_main
SET deleted_at = NOW()
WHERE status = 0 AND create_time < '2026-01-01';
这条 SQL 是用来清理无效订单的,结果误把一批本该保留的订单也标记了。但运维同学当时用的是 Navicat 图形化操作,没在飞书上发通知,所以后端没人知道这批数据被软删了。
💡 解决方案
方案一:临时绕过(紧急恢复数据)
先把误删的数据恢复:
UPDATE order_main
SET deleted_at = NULL
WHERE order_no IN (
'ORD20260318001',
-- 其他需要恢复的订单号列表
);
方案二:禁用单表的逻辑删除(推荐)
不需要逻辑删除的表,在实体类上显式关闭:
@Data
@TableName(value = "order_main", logicDelete = false)
public class OrderMain {
// ...
}
方案三:使用 @TableLogic 注解精细化控制
如果某些表确实需要逻辑删除,用注解明确指定,而不是依赖全局配置:
@Data
@TableName("order_main")
public class OrderMain {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
// 显式声明逻辑删除字段
@TableLogic(deleted = "1", undeleted = "0")
private Integer deleted;
// ...
}
方案四:在 MyBatis-Plus 配置中缩小影响范围
如果不使用全局逻辑删除字段配置,而是逐表声明,可以让配置更可控:
# application.yml
mybatis-plus:
global-config:
db-config:
# 移除全局逻辑删除配置
# logic-delete-field: deletedAt
id-type: assign_id
📊 性能与安全权衡
逻辑删除本身是个好设计,好处包括:数据可恢复、不破坏历史关联关系、满足审计要求。但使用不当会引发三类问题:
- 隐式查询过滤:开发者不知道查询被加了条件,容易产生”数据丢失”的困惑。
- 唯一索引失效:软删数据占用唯一索引位置,可能导致唯一约束冲突。
- 数据积累:大量软删数据堆积,影响查询性能,需要定期清理。
我的经验是:核心业务表(订单、支付、用户)慎用逻辑删除,用物理删除 + 日志表替代;配置类、字典类数据可以用逻辑删除。
🛠 工具链排查技巧
遇到类似问题,推荐以下排查路径:
# 1. 开启 SQL 日志,观察实际执行的 SQL
logging:
level:
com.example.mapper: DEBUG
# 2. 查看数据库中该表所有数据(含软删)
SELECT * FROM order_main WHERE id = 'ORD20260318001';
-- 或
SELECT * FROM order_main WHERE id = 'ORD20260318001' AND deleted_at IS NOT NULL;
# 3. 查看表结构确认字段名
DESC order_main;
# 4. 检查 MP 版本及全局配置
# 在 Spring Boot 启动类或测试类中打印配置
@Autowired
private GlobalConfig globalConfig;
System.out.println(globalConfig.getDbConfig());
✅ 总结
- MyBatis-Plus 的全局逻辑删除配置会影响所有表,除非显式用
logicDelete = false关闭。 - 实体类没有声明逻辑删除字段时,MP 仍会按全局配置自动注入过滤条件——这个行为非常隐蔽。
- 生产环境操作数据库(尤其是 UPDATE/DELETE)务必通知到后端团队,并留下操作记录。
- 核心业务数据优先物理删除 + 归档日志表,逻辑删除仅用于非核心配置表。
这个坑前后花了大约 3 个小时才定位清楚,希望你遇到类似情况时能少走弯路。
⚠️ 声明:本文相关内容仅供参考,如有实际业务需求请结合自身情况谨慎评估。