MyBatis-Plus逻辑删除配置踩坑记:从yml到@TableLogic,我为什么最终选择了全局配置?

张开发
2026/4/20 14:20:39 15 分钟阅读

分享文章

MyBatis-Plus逻辑删除配置踩坑记:从yml到@TableLogic,我为什么最终选择了全局配置?
MyBatis-Plus逻辑删除配置实战从注解到全局配置的工程化思考第一次在团队项目中使用MyBatis-Plus的逻辑删除功能时我像发现新大陆一样兴奋——终于不用在每个查询里手动加where deleted0了但很快当看到同事在实体类里随意添加TableLogic注解时我才意识到技术选型只是开始如何规范使用才是真正的挑战。1. 逻辑删除的本质与价值逻辑删除Logical Delete是现代数据库设计中常见的软删除策略。与物理删除直接抹去数据不同它通过标记字段的状态来模拟删除行为。这种设计在以下场景中尤其重要审计合规金融、医疗等行业需要完整的数据历史记录误操作恢复避免手滑删除关键业务数据关联数据保护防止级联删除破坏数据完整性MyBatis-Plus的优雅之处在于它用最简化的配置实现了自动化的逻辑删除处理。但正是这种过于方便的特性容易让人忽视背后的工程隐患。2. 两种配置方式的深度对比2.1 注解式配置灵活的表象下藏着陷阱Data public class User { TableLogic(value 1, delval 0) private Integer isDeleted; }这种配置看起来简单直接但在实际项目中会暴露诸多问题字段命名混乱有的用deleted有的用is_deleted还有用del_flag状态值不统一1/0、true/false、Y/N等多种表示方式并存维护成本高新增实体时需要重复配置修改逻辑时要全局搜索提示我曾在一个老项目中发现了7种不同的逻辑删除字段命名导致每次联调都要额外处理字段映射。2.2 全局配置约束带来的自由mybatis-plus: global-config: db-config: logic-delete-field: deleted # 统一字段名 logic-delete-value: 1 # 删除状态值 logic-not-delete-value: 0 # 未删除状态值全局配置的优势体现在维度注解配置全局配置一致性依赖开发人员自觉强制统一标准可维护性修改需要逐个实体调整一处修改全局生效可读性需要查看实体类定义配置集中一目了然团队协作容易产生风格差异统一规范减少沟通成本3. 那些年我踩过的坑3.1 自定义SQL的漏网之鱼Select(SELECT * FROM user WHERE age #{age}) ListUser selectByAge(Param(age) int age);这样的自定义查询会绕过MP的逻辑删除过滤解决方案有两种手动添加条件Select(SELECT * FROM user WHERE age #{age} AND deleted 0)使用Wrapper构建userMapper.selectList(Wrappers.Userquery() .gt(age, 18) .eq(deleted, 0));3.2 联表查询时的注意事项当进行多表关联查询时每张表的逻辑删除字段都需要处理SELECT u.*, d.name FROM user u JOIN department d ON u.dept_id d.id WHERE u.deleted 0 AND d.deleted 0建议为所有表使用相同的逻辑删除字段名可以大幅降低SQL编写复杂度。4. 工程化实践建议4.1 数据库设计规范统一使用deleted作为字段名而非is_deleted等变体采用tinyint类型0表示未删除1表示已删除所有业务表必须包含该字段4.2 项目中的最佳实践初始化脚本模板CREATE TABLE example_table ( id bigint NOT NULL AUTO_INCREMENT, -- 其他字段 deleted tinyint NOT NULL DEFAULT 0 COMMENT 0未删除 1已删除, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;全局异常处理ExceptionHandler(SQLException.class) public Result handleSQLException(SQLException e) { if (e.getMessage().contains(deleted)) { return Result.fail(逻辑删除字段异常请检查配置); } // 其他处理... }审计日志集成Interceptor public class LogicDeleteAuditInterceptor implements InnerInterceptor { Override public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) { // 记录逻辑删除操作日志 } }5. 进阶场景解决方案5.1 历史数据查询方案有时需要查询已被逻辑删除的数据// 方案1使用SQL注入器 SqlParser(filter true) Select(SELECT * FROM user WHERE id #{id}) User selectIncludeDeleted(Long id); // 方案2临时关闭逻辑删除 User user new User(); user.setId(1); SqlHelper.removeLogicDelete(user.getClass()); userMapper.selectById(user); SqlHelper.restoreLogicDelete(user.getClass());5.2 多租户下的特殊处理在多租户系统中可能需要组合租户ID和逻辑删除条件public void buildTenantLogicDeleteWrapper(Wrapper? wrapper) { String tenantColumn getTenantColumn(); String logicDeleteColumn getLogicDeleteColumn(); wrapper.eq(StringUtils.isNotBlank(tenantColumn), tenantColumn, getTenantId()) .eq(StringUtils.isNotBlank(logicDeleteColumn), logicDeleteColumn, 0); }6. 性能优化技巧索引优化ALTER TABLE user ADD INDEX idx_deleted (deleted);查询优化避免在逻辑删除字段上使用IS NULL判断大数据量表考虑分区策略按deleted值分区缓存策略Cacheable(unless #result?.deleted 1) public User getById(Long id) { return userMapper.selectById(id); }从个人项目到团队协作从功能实现到工程规范逻辑删除这个看似简单的功能教会我最重要的一课好的技术方案不仅要能工作更要能持续稳定地工作。全局配置可能看起来不够灵活但正是这种约束让我们的系统在迭代中保持了可维护性。

更多文章