Java框架快速入门:深入MyBatis-Plus高级DML操作(ID策略·逻辑删除·乐观锁)
2026/7/12 19:49:42 网站建设 项目流程

本文纲要

一、ID生成策略
1.1 常见ID生成场景
1.2 使用@TableId指定策略
1.3IdType枚举详解
1.4 深入雪花算法
1.5 全局配置统一ID策略与表前缀

二、多数据删除与查询
2.1 批量删除
2.2 批量查询

三、逻辑删除
3.1 物理删除的痛点
3.2 逻辑删除解决方案
3.3 实现逻辑删除
3.4 全局配置逻辑删除
3.5 逻辑删除对查询的影响

四、乐观锁
4.1 并发问题背景
4.2 乐观锁实现步骤
4.3 乐观锁拦截器配置
4.4 并发修改模拟
总结

引言

在日常开发中,除了基础的数据查询,增删改(DML)操作更是业务核心。不同的业务场景对主键生成、删除方式、并发控制等有着截然不同的要求。MyBatis-Plus 提供了一系列注解和插件机制,帮助开发者用极简的方式应对这些复杂场景。

本文将深入讲解 ID 生成策略、多数据操作、逻辑删除和乐观锁,并给出完整的示例代码。

项目结构概览

本文示例项目基于Spring Boot+MyBatis-Plus,目录结构如下:

mybatisplus_03_dml ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com/wb │ │ │ ├── Mybatisplus03DqlApplication.java │ │ │ ├── config │ │ │ │ └── MpConfig.java │ │ │ ├── dao │ │ │ │ └── UserDao.java │ │ │ └── domain │ │ │ └── User.java │ │ └── resources │ │ ├── application.yml │ │ └── logback.xml │ └── test │ └── java │ └── com/wb │ └── Mybatisplus03DqlApplicationTests.java

一、ID生成策略

1 ) 常见ID生成场景

在数据库设计中,主键的生成方式直接影响业务维护和扩展性,常见需求包括:

  • 简单自增:如日志表,直接使用数据库自增即可。
  • 自定义字符串:购物订单号可能包含字母、日期、区域编码等,由程序手动生成。
  • 特定规则组合:外卖订单往往包含城市码、区号、日期、流水号等一系列规则,最后组合并加上校验位。
  • 关联表省略主键:多对多关系表通常只使用两个外键,主键可以省略甚至不使用。

面对如此多元的需求,MyBatis-Plus 允许我们灵活控制主键的生成方式。

2 ) 使用@TableId指定策略

在实体类字段上使用@TableId注解,即可指定主键生成策略。

// domain/User.java (局部)packagecom.wb.domain;importcom.baomidou.mybatisplus.annotation.IdType;importcom.baomidou.mybatisplus.annotation.TableId;importlombok.Data;@DatapublicclassUser{@TableId(type=IdType.AUTO)// 使用数据库自增privateLongid;privateStringname;// ... 其他字段}

设置type = IdType.AUTO后,程序不再生成主键,而是依赖数据库的自增能力。
运行插入操作前,将测试表中自增初始值改小,执行后发现新记录的 id 确实遵循了自增规则。

3 ) IdType 枚举详解

IdType是一个枚举类,提供了多种策略:

枚举值含义说明
AUTO数据库自增依赖数据库自增,不需设置 id
NONE无状态未设置主键策略(默认跟随全局)
INPUT用户输入需要开发者手动 set id
ASSIGN_ID雪花算法生成 Long默认策略,生成 19 位长整数
ASSIGNUUID雪花算法生成 String与 ASSIGNID 类似,但生成字符串
IDWORKER已过时,合并到 ASSIGNID
IDWORKERSTR已过时,合并到 ASSIGN_UUID
UUID已过时,原指 32 位 UUID现在统一使用以上策略

INPUT 示例

Useruser=newUser();user.setId(666L);// 手动指定 iduser.setName("程序员");user.setPassword("wb");userDao.insert(user);

执行 insert 时,SQL 直接使用该 id。若未设置且数据库字段不允许为空,则会报错。

// 测试类 testSave()@TestvoidtestSave(){Useruser=newUser();// user.setId(667L); // 注释后,将使用全局策略或雪花算法user.setName("程序员");user.setPassword("wb");user.setAge(12);user.setTel("4006184000");userDao.insert(user);}

ASSIGN_ID(雪花算法默认策略)

@TableId(type=IdType.ASSIGN_ID)privateLongid;

插入时不设置 id,MP 会自动生成一个长整型 id,如1402551342481838081

4 ) 深入雪花算法

雪花算法(Snowflake)生成的 id 是一个 64 位二进制数(即 Java 的long类型)。其结构如下:

64位

1位 符号位
恒为0

41位 时间戳
毫秒级

10位 机器标识
5位机房 + 5位机器

12位 序列号
同毫秒内自增

  • 第1位:符号位,恒为0,保证 id 为正数。
  • 第2-42位:41位时间戳,精确到毫秒。注意这是雪花内部时间,不是 Unix 时间戳。
  • 第43-52位:10位机器码,前5位代表数据中心(群组),后5位代表工作机器(类 MAC 地址)。可部署 1024 台机器。
  • 第53-64位:12位序列号,同一毫秒内最多支持 4096 个 id。

这样的设计使得分布式环境下,每台机器、每一毫秒都能生成不重复的 id。使用bigint类型存储即可。

5 ) 全局配置统一ID策略与表前缀

如果每个实体类都要写@TableId@TableName,会非常繁琐。MP 支持全局配置:

application.yml (局部)mybatis-plus:global-config:db-config:id-type:assign_id# 全局主键策略table-prefix:tbl# 表名前缀,实体类 User → 表 tbluserlogic-delete-field:deleted# 逻辑删除字段(后续介绍)logic-not-delete-value:0# 未删除标记值logic-delete-value:1# 已删除标记值

配置table-prefix后,User类会自动映射到tb_luser表,无需再写@TableName("tbluser")
同样,id-type统一了所有实体的主键生成策略。

二、多数据删除与查询

1 ) 批量删除

MP 提供了deleteBatchIds方法,可一次性删除多条记录。

@TestvoidtestDelete(){List<Long>ids=newArrayList<>();ids.add(1402551342481838081L);ids.add(1402553134049501186L);ids.add(1402553619611430913L);introws=userDao.deleteBatchIds(ids);System.out.println("删除了 "+rows+" 条数据");}

执行时,后台生成DELETE FROM tbl_user WHERE id IN (?,?,?)语句,高效完成批量删除。

2 ) 批量查询

类似地,selectBatchIds接收一个 id 集合,返回对应的实体列表。

@TestvoidtestBatchSelect(){List<Long>ids=newArrayList<>();ids.add(1L);ids.add(3L);ids.add(4L);List<User>users=userDao.selectBatchIds(ids);users.forEach(System.out::println);}

无论删除还是查询,只要提供 id 集合,MP 就能一次性完成操作。

三、逻辑删除

1 ) 物理删除的痛点

假设一个房屋中介销售系统:员工 张业绩 离职后,他的信息被物理删除(DELETE),与之关联的开单记录变成脏数据,导致年底统计报表数据失真。

老板质问:“明明开了几十万的大单,为什么只统计到十万?” 问题根源就在于物理删除直接摧毁了业务数据。

2 ) 逻辑删除解决方案

逻辑删除并不真正删除数据,而是在表中增加一个状态字段,标识记录是否被“删除”。

  • 状态为0:正常,未删除。
  • 状态为1:已删除(隐藏,不参与常规查询)。

这样,历史数据得以保留,统计报表不受影响。

3 )实现逻辑删除

三步走:数据库加字段 → 实体类加字段并注解 → 测试效果。

3.1 数据库添加字段

ALTERTABLEtbl_userADDCOLUMNdeletedINTDEFAULT0COMMENT'逻辑删除(0未删,1已删)';

3.2 实体类配置

// domain/User.java (局部)importcom.baomidou.mybatisplus.annotation.TableLogic;@DatapublicclassUser{// ...其他字段@TableLogic(value="0",delval="1")privateIntegerdeleted;}
  • value:未删除的取值(对应数据库默认 0)。
  • delval:删除后的取值(1)。

3.3 执行删除测试

userDao.deleteById(2L);

控制台打印的 SQL 变为:

UPDATEtbl_userSETdeleted=1WHEREid=2ANDdeleted=0

数据库记录仍在,只是deleted字段被标记为 1

3.4 全局配置逻辑删除

为避免每个实体类都重复注解,可在application.yml中统一指定字段名和状态值。

mybatis-plus:global-config:db-config:logic-delete-field:deletedlogic-not-delete-value:0logic-delete-value:1

此后所有实体类只需包含private Integer deleted;字段,无需添加@TableLogic
执行删除时,MP 自动识别

# 完整配置示例spring:datasource:driver-class-name:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTCusername:rootpassword:rootmybatis-plus:configuration:log-impl:org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:id-type:assign_idtable-prefix:tbl_logic-delete-field:deletedlogic-not-delete-value:0logic-delete-value:1

3.5 逻辑删除对查询的影响

开启逻辑删除后,所有 MP 自动生成的查询 SQL 都会自动追加AND deleted = 0条件。

List<User>users=userDao.selectList(null);// SQL: SELECT id,name,... FROM tbl_user WHERE deleted=0

被标记为已删除的记录将不会出现在查询结果中。如果需要查询全部数据(包括已删除),需要自定义 SQL 绕过 MP 的自动条件。

四、乐观锁

1 )并发问题背景

并发问题在秒杀、抢购等业务中尤为突出。例如,库存只有 1 件,多个用户同时下单,若不加以控制,会出现超卖(负库存)。乐观锁正是应对此类并发的一种策略,适用于并发冲突不太频繁的场景。

2 )乐观锁实现步骤

乐观锁的思想:给数据增加一个“版本号”字段,每次更新时检查版本号。若版本号与读取时一致,则更新并将版本号加一;若不一致,则说明数据已被他人修改,本次更新失败。

实现流程:

用户B数据库用户A用户B数据库用户A查询 id=3, version=3查询 id=3, version=3update name='Jock aaa' where id=3 and version=3 (成功,version变为4)update name='Jock bbb' where id=3 and version=3 (失败,version已变)

在 MP 中,乐观锁的实现依赖@Version注解和拦截器。

数据库添加 version 字段

ALTERTABLEtbl_userADDCOLUMNversionINTDEFAULT1COMMENT'乐观锁版本号';

实体类添加version属性并注解

importcom.baomidou.mybatisplus.annotation.Version;@DatapublicclassUser{// ...其他字段@VersionprivateIntegerversion;}

拦截器配置

// config/MpConfig.javapackagecom.wb.config;importcom.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;importcom.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;importcom.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassMpConfig{@BeanpublicMybatisPlusInterceptormpInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();// 分页拦截器interceptor.addInnerInterceptor(newPaginationInnerInterceptor());// 乐观锁拦截器interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());returninterceptor;}}
  • OptimisticLockerInnerInterceptor负责在 update 语句中自动拼接where version = ?并将version加 1。

3 ) 乐观锁使用示例

修改数据时必须保证实体中携带version值,通常做法是先查询再修改:

@TestvoidtestUpdateWithLock(){// 1. 先查询Useruser=userDao.selectById(3L);// version=3// 2. 修改属性user.setName("Jock888");// 3. 更新userDao.updateById(user);}

执行 SQL:

UPDATEtbl_userSETname='Jock888',version=4WHEREid=3ANDversion=3

若另一个人抢先修改了该记录,version 变为 4,则上述WHERE version=3不成立,更新行数为 0,不会覆盖他人修改。

4 ) 并发修改模拟

编写测试代码模拟两个用户并发修改同一条数据:

@TestvoidtestConcurrentUpdate(){// 模拟两个用户同时读取UseruserA=userDao.selectById(3L);// version=3UseruserB=userDao.selectById(3L);// version=3// B 先更新userB.setName("Jock aaa");userDao.updateById(userB);// 成功,version 变为 4// A 后更新userA.setName("Jock bbb");userDao.updateById(userA);// 失败,因为 version=3 不再匹配}

运行后数据库记录为Jock aaa,说明 B 的更新成功,A 的更新因版本过期而被忽略,有效防止了数据覆盖。

总结

本文以实战为导向,全面介绍了 MyBatis-Plus 在增删改操作中的高级特性:

模块核心注解/配置作用
ID生成@TableId(type=...)灵活控制主键生成策略
多数据操作deleteBatchIds / selectBatchIds批量处理数据
逻辑删除@TableLogic或全局配置logic-delete-field标记删除,保留数据
乐观锁@Version + OptimisticLockerInnerInterceptor解决并发更新冲突

掌握这些技巧,能让你在面对复杂业务需求时从容应对,写出更健壮、更易维护的代码。
项目完整源码可参考前面给出的目录结构,各文件均提供了可直接运行的完整内容。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询