一文搞懂:MyBatis-Plus高级特性实战——逻辑删除、乐观锁、自动填充
📌 写在前面
刚接触MyBatis-Plus时,大多数人和我一样,只会用BaseMapper提供的基础CRUD方法,最多再加个分页插件。至于逻辑删除、乐观锁、自动填充这些高级特性,好像一直停留在“听说过,没用过”的阶段。
直到最近做一个电商项目,遇到几个棘手的场景:用户数据不能物理删除,需要保留历史痕迹;商品库存扣减时出现了超卖问题;每次写业务代码都要手动设置createTime和updateTime,既繁琐又容易遗漏……这才发现,原来MyBatis-Plus早就帮我封装好了解决方案。
这篇笔记,我就从实战出发,梳理MyBatis-Plus最实用的五个高级特性:逻辑删除、乐观锁、自动填充、性能分析插件、多租户支持。希望能帮你从“只会用基础功能”进阶到“精通企业级特性”。

1️⃣ 逻辑删除:删除只改标记,数据永不丢失
什么是逻辑删除?
物理删除是直接从数据库中DELETE掉记录,数据彻底消失。逻辑删除则是通过一个状态字段(如del_flag、is_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支持的数据类型包括:int、Integer、long、Long、Date、Timestamp、LocalDateTime。
第三步:配置乐观锁插件
@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_time、update_time这样的公共字段,每次插入或更新都需要手动设置,既繁琐又容易遗漏。MyBatis-Plus的自动填充功能可以在执行INSERT或UPDATE时自动为这些字段赋值,彻底解放双手。
实战配置
第一步:数据库添加字段
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_by、update_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️⃣ 避坑指南
逻辑删除篇
-
唯一索引冲突:逻辑删除后,再次插入相同唯一键的数据会冲突。解决方案:将删除标记字段纳入联合唯一索引
-
自定义SQL需手动过滤:在XML中写的SQL不会自动添加
WHERE deleted=0,需要自己加 -
@TableField(exist = false)与@TableLogic混淆:前者用于标记非数据库字段,后者才是逻辑删除,别用混了
乐观锁篇
-
重试机制:更新失败时需要重新查询最新数据再试,一般重试3-5次
-
批量操作不支持:
updateBatchById等批量更新方法中的乐观锁校验需要额外处理 -
版本号必须递增:整数类型会自动
version+1,但自定义更新方法需手动维护
自动填充篇
-
updateWrapper失效:使用
update(Wrapper)时自动填充不会生效,需手动赋值 -
属性有值时不覆盖:如果想强制覆盖,需要先手动设为
null -
类型匹配:
strictInsertFill会根据字段类型进行安全填充,推荐使用
性能分析篇
-
切勿在生产环境长期开启:会带来额外的性能开销,建议只在开发和测试环境使用
-
合理设置超时阈值:过低的阈值会产生大量误报,建议根据业务基线设置
多租户篇
-
忽略表配置:系统配置表、字典表等需要全局共享的表,记得在
ignoreTable中排除 -
租户ID传递:确保租户ID能从请求上下文中正确获取,建议使用
ThreadLocal传递 -
分页兼容:多租户插件和分页插件同时使用时,注意拦截器的注册顺序
📌 写在最后
回到最初的问题:为什么这些高级特性总觉得“没用过”?因为大多数项目的复杂度和并发量还没到需要它们的程度。但随着业务增长,你会发现:
-
当用户要求“删错了能恢复”时,逻辑删除就是答案
-
当商品超卖发生时,乐观锁就是救星
-
当代码里充斥着
setCreateTime(new Date())时,自动填充就是解药
这些特性并不复杂,配置一次,终身受益。建议你在下一个项目中主动用起来,别等到出问题了才想起来。
下一篇,我计划写一篇关于MyBatis-Plus条件构造器深度解析,从QueryWrapper到LambdaQueryWrapper,再到复杂动态SQL的构建技巧,敬请期待。
更多推荐



所有评论(0)