📌 写在前面

        刚接触MyBatis-Plus时,大多数人和我一样,只会用BaseMapper提供的基础CRUD方法,最多再加个分页插件。至于逻辑删除、乐观锁、自动填充这些高级特性,好像一直停留在“听说过,没用过”的阶段。

        直到最近做一个电商项目,遇到几个棘手的场景:用户数据不能物理删除,需要保留历史痕迹;商品库存扣减时出现了超卖问题;每次写业务代码都要手动设置createTimeupdateTime,既繁琐又容易遗漏……这才发现,原来MyBatis-Plus早就帮我封装好了解决方案

        这篇笔记,我就从实战出发,梳理MyBatis-Plus最实用的五个高级特性:逻辑删除、乐观锁、自动填充、性能分析插件、多租户支持。希望能帮你从“只会用基础功能”进阶到“精通企业级特性”。

1️⃣ 逻辑删除:删除只改标记,数据永不丢失

什么是逻辑删除?

物理删除是直接从数据库中DELETE掉记录,数据彻底消失。逻辑删除则是通过一个状态字段(如del_flagis_deleted)来标记数据是否被删除,查询时自动过滤已删除的数据。

为什么需要逻辑删除?

  • 数据可恢复:误删后改回标记即可

  • 审计追踪:保留数据变更历史

  • 业务连续性:关联数据不会因为物理删除而断裂

实战配置

第一步:数据库添加字段

ALTER TABLE `user` 
ADD COLUMN `deleted` INT DEFAULT 0 COMMENT '删除标识(0-正常 1-删除)';

第二步:实体类添加@TableLogic注解

@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    
    @TableLogic(value = "0", delval = "1")
    private Integer deleted;
}

value表示未删除时的值(默认0),delval表示已删除时的值(默认1),可按需修改。

第三步:全局配置(可选)

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted   # 全局逻辑删除字段名
      logic-not-delete-value: 0     # 未删除值
      logic-delete-value: 1         # 已删除值

注解配置优先级高于全局配置。

使用效果

执行删除操作时,MyBatis-Plus会自动将DELETE转换为UPDATE

userMapper.deleteById(1L);
-- 实际执行SQL:UPDATE user SET deleted=1 WHERE id=1 AND deleted=0

查询时自动过滤已删除数据,无需手动加条件:

List<User> list = userMapper.selectList(null);
-- 生成的SQL自动包含 WHERE deleted=0

高级技巧

  • 自定义SQL需手动处理:在XML中写查询时,记得加上WHERE deleted=0条件

  • 唯一索引优化:逻辑删除可能导致唯一约束冲突,建议将删除标记字段纳入联合唯一索引

  • 临时关闭逻辑删除:如需查询已删除数据,可使用@InterceptorIgnore(logicDelete = "true")

  • 支持非数字类型@TableLogic(value = "ACTIVE", delval = "DELETED")可用于字符串状态字段

个人建议:逻辑删除几乎适用于所有业务表,尤其是用户、订单等核心数据。唯一的例外是日志表、临时表等,物理删除反而更合适。

2️⃣ 乐观锁:高并发下的防超卖利器

什么是乐观锁?

乐观锁是一种基于版本控制的并发解决方案。它假设数据更新冲突的概率较低,因此不对数据加锁,而是在更新时校验版本号是否发生变化——若一致则更新成功并将版本号+1,若不一致则说明数据已被别人修改,更新失败

线程A读取version=1 → 更新时检查version=1 → 更新成功,version变为2
线程B读取version=1 → 更新时发现version已变成2 → 更新失败,需重试

相比悲观锁(SELECT FOR UPDATE),乐观锁不会阻塞数据库,性能更高,适合读多写少的业务场景。

实战配置

第一步:数据库添加version字段

ALTER TABLE `product` 
ADD COLUMN `version` INT NOT NULL DEFAULT 0 COMMENT '版本号';

第二步:实体类添加@Version注解

@Data
@TableName("product")
public class Product {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer stock;
    
    @Version
    private Integer version;
}

MyBatis-Plus支持的数据类型包括:intIntegerlongLongDateTimestampLocalDateTime

第三步:配置乐观锁插件

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

使用效果

// 1. 查询商品,拿到当前version
Product product = productMapper.selectById(1L);
// product.version = 5

// 2. 执行业务逻辑(如扣减库存)
product.setStock(product.getStock() - 1);

// 3. 更新时自动校验version
int result = productMapper.updateById(product);
// 生成的SQL:UPDATE product SET stock=?, version=version+1 
//            WHERE id=? AND version=5
// 如果result==0,说明version已被修改,需要重试

更新成功后,MP会自动将新版本号回填到实体对象中。

适用场景

3️⃣ 自动填充:告别手写createTime/updateTime

什么是自动填充?

在数据库操作中,像create_timeupdate_time这样的公共字段,每次插入或更新都需要手动设置,既繁琐又容易遗漏。MyBatis-Plus的自动填充功能可以在执行INSERTUPDATE时自动为这些字段赋值,彻底解放双手。

实战配置

第一步:数据库添加字段

ALTER TABLE `user` 
ADD COLUMN `create_time` DATETIME COMMENT '创建时间',
ADD COLUMN `update_time` DATETIME COMMENT '更新时间';

第二步:实体类配置@TableField

@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

FieldFill枚举说明:

  • INSERT:仅插入时填充

  • UPDATE:仅更新时填充

  • INSERT_UPDATE:插入和更新时都填充

第三步:实现MetaObjectHandler

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    
    @Override
    public void insertFill(MetaObject metaObject) {
        // 插入时填充创建时间和更新时间
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
    
    @Override
    public void updateFill(MetaObject metaObject) {
        // 更新时只填充更新时间
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

注意事项

  • 属性有值时不覆盖:如果实体类的字段已经有值,自动填充不会覆盖,确保手动赋值的优先级更高

  • updateWrapper方式需注意:使用update(Wrapper)时自动填充会失效,需要手动赋值

  • 推荐使用strictInsertFill:能根据字段类型进行安全填充,避免类型不匹配问题

实用技巧:除了时间字段,还可以用自动填充实现操作人信息(create_byupdate_by)、租户ID等公共字段的统一管理。

4️⃣ 性能分析插件:揪出慢SQL的利器

功能介绍

MyBatis-Plus内置了性能分析插件,可以输出每条SQL语句及其执行时间,帮助开发者快速定位慢查询。官方建议在开发测试环境启用该功能,能快速揪出性能瓶颈。

⚠️ 版本提醒PerformanceInterceptor在MyBatis-Plus 3.2.0及以上版本中已被移除,推荐使用P6Spy作为替代方案。

方案一:druid过滤器

如果项目使用Alibaba Druid连接池,可以开启慢SQL监控:

spring:
  datasource:
    druid:
      filters: stat
      filter:
        stat:
          slow-sql-millis: 1000
          log-slow-sql: true

方案二:自定义MyBatis拦截器

对于不想引入额外依赖的项目,可以自己实现一个简单的SQL拦截器,记录执行时间。

个人建议P6Spy + 慢SQL阈值是最佳组合,既能看执行时间,又能在生产环境控制性能开销。

5️⃣ 多租户支持:SaaS应用的数据隔离方案

什么是多租户?

多租户是一种软件架构模式:同一套程序服务于多个客户(租户),各租户的数据相互隔离。MyBatis-Plus的TenantLineInnerInterceptor插件可以在执行SQL时自动添加租户ID过滤条件,让开发者像操作单租户系统一样开发。

实战配置

第一步:数据库添加tenant_id字段

ALTER TABLE `order` 
ADD COLUMN `tenant_id` BIGINT NOT NULL COMMENT '租户ID';

第二步:实体类添加字段

@Data
@TableName("order")
public class Order {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String orderNo;
    private Long tenantId;  // 租户标识
}

第三步:实现租户处理器

@Component
public class CustomTenantHandler implements TenantLineHandler {
    
    @Override
    public Expression getTenantId() {
        // 从当前上下文中获取租户ID(如从请求头或SecurityContext中获取)
        Long tenantId = TenantContextHolder.getCurrentTenantId();
        return new LongValue(tenantId);
    }
    
    @Override
    public String getTenantIdColumn() {
        return "tenant_id";  // 租户字段名,默认就是tenant_id
    }
    
    @Override
    public boolean ignoreTable(String tableName) {
        // 指定哪些表不需要多租户过滤(如系统配置表)
        return "sys_config".equalsIgnoreCase(tableName);
   
 }
}

第四步:配置插件

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new CustomTenantHandler()));
        return interceptor;
    }
}

工作原理

原始SQL:

SELECT * FROM `order` WHERE status = 'PAID'

插件自动注入租户条件后:

SELECT * FROM `order` WHERE status = 'PAID' AND tenant_id = 1001

注意事项

  • 联合自动填充使用:插入数据时需要配合自动填充功能设置tenant_id字段,否则该字段可能为空

  • 索引优化:确保tenant_id字段有索引,最好作为联合索引的第一列

  • 排除系统表:使用ignoreTable方法过滤不需要租户隔离的表

6️⃣ 综合对比:特性定位与选型建议

一句话总结

  • 逻辑删除自动填充几乎是所有项目的标配,建议从一开始就引入

  • 乐观锁是解决高并发更新问题的标准方案,推荐在核心业务表中使用

  • 性能分析插件在开发阶段价值巨大,能提前发现性能隐患

  • 多租户是SaaS应用的必备能力,但普通项目不需要

7️⃣ 避坑指南

逻辑删除篇

  1. 唯一索引冲突:逻辑删除后,再次插入相同唯一键的数据会冲突。解决方案:将删除标记字段纳入联合唯一索引

  2. 自定义SQL需手动过滤:在XML中写的SQL不会自动添加WHERE deleted=0,需要自己加

  3. @TableField(exist = false)@TableLogic混淆:前者用于标记非数据库字段,后者才是逻辑删除,别用混了

乐观锁篇

  1. 重试机制:更新失败时需要重新查询最新数据再试,一般重试3-5次

  2. 批量操作不支持updateBatchById等批量更新方法中的乐观锁校验需要额外处理

  3. 版本号必须递增:整数类型会自动version+1,但自定义更新方法需手动维护

自动填充篇

  1. updateWrapper失效:使用update(Wrapper)时自动填充不会生效,需手动赋值

  2. 属性有值时不覆盖:如果想强制覆盖,需要先手动设为null

  3. 类型匹配strictInsertFill会根据字段类型进行安全填充,推荐使用

性能分析篇

  1. 切勿在生产环境长期开启:会带来额外的性能开销,建议只在开发和测试环境使用

  2. 合理设置超时阈值:过低的阈值会产生大量误报,建议根据业务基线设置

多租户篇

  1. 忽略表配置:系统配置表、字典表等需要全局共享的表,记得在ignoreTable中排除

  2. 租户ID传递:确保租户ID能从请求上下文中正确获取,建议使用ThreadLocal传递

  3. 分页兼容:多租户插件和分页插件同时使用时,注意拦截器的注册顺序

📌 写在最后

        回到最初的问题:为什么这些高级特性总觉得“没用过”?因为大多数项目的复杂度和并发量还没到需要它们的程度。但随着业务增长,你会发现:

  • 当用户要求“删错了能恢复”时,逻辑删除就是答案

  • 当商品超卖发生时,乐观锁就是救星

  • 当代码里充斥着setCreateTime(new Date())时,自动填充就是解药

        这些特性并不复杂,配置一次,终身受益。建议你在下一个项目中主动用起来,别等到出问题了才想起来。

        下一篇,我计划写一篇关于MyBatis-Plus条件构造器深度解析,从QueryWrapperLambdaQueryWrapper,再到复杂动态SQL的构建技巧,敬请期待。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐