Java框架快速入门:深入MyBatis-Plus高级DML操作(ID策略·逻辑删除·乐观锁)

发布时间:2026/7/12 19:49:45
Java框架快速入门:深入MyBatis-Plus高级DML操作(ID策略·逻辑删除·乐观锁) 本文纲要一、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 BootMyBatis-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(typeIdType.AUTO)// 使用数据库自增privateLongid;privateStringname;// ... 其他字段}设置type IdType.AUTO后程序不再生成主键而是依赖数据库的自增能力。运行插入操作前将测试表中自增初始值改小执行后发现新记录的 id 确实遵循了自增规则。3 ) IdType 枚举详解IdType是一个枚举类提供了多种策略枚举值含义说明AUTO数据库自增依赖数据库自增不需设置 idNONE无状态未设置主键策略默认跟随全局INPUT用户输入需要开发者手动 set idASSIGN_ID雪花算法生成 Long默认策略生成 19 位长整数ASSIGNUUID雪花算法生成 String与 ASSIGNID 类似但生成字符串IDWORKER已过时合并到 ASSIGNIDIDWORKERSTR已过时合并到 ASSIGN_UUIDUUID已过时原指 32 位 UUID现在统一使用以上策略INPUT 示例UserusernewUser();user.setId(666L);// 手动指定 iduser.setName(程序员);user.setPassword(wb);userDao.insert(user);执行 insert 时SQL 直接使用该 id。若未设置且数据库字段不允许为空则会报错。// 测试类 testSave()TestvoidtestSave(){UserusernewUser();// user.setId(667L); // 注释后将使用全局策略或雪花算法user.setName(程序员);user.setPassword(wb);user.setAge(12);user.setTel(4006184000);userDao.insert(user);}ASSIGN_ID雪花算法默认策略TableId(typeIdType.ASSIGN_ID)privateLongid;插入时不设置 idMP 会自动生成一个长整型 id如1402551342481838081。4 ) 深入雪花算法雪花算法Snowflake生成的 id 是一个 64 位二进制数即 Java 的long类型。其结构如下64位1位 符号位恒为041位 时间戳毫秒级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(){ListLongidsnewArrayList();ids.add(1402551342481838081L);ids.add(1402553134049501186L);ids.add(1402553619611430913L);introwsuserDao.deleteBatchIds(ids);System.out.println(删除了 rows 条数据);}执行时后台生成DELETE FROM tbl_user WHERE id IN (?,?,?)语句高效完成批量删除。2 ) 批量查询类似地selectBatchIds接收一个 id 集合返回对应的实体列表。TestvoidtestBatchSelect(){ListLongidsnewArrayList();ids.add(1L);ids.add(3L);ids.add(4L);ListUserusersuserDao.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(value0,delval1)privateIntegerdeleted;}value未删除的取值对应数据库默认 0。delval删除后的取值1。3.3 执行删除测试userDao.deleteById(2L);控制台打印的 SQL 变为UPDATEtbl_userSETdeleted1WHEREid2ANDdeleted0数据库记录仍在只是deleted字段被标记为 13.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?serverTimezoneUTCusername: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:13.5 逻辑删除对查询的影响开启逻辑删除后所有 MP 自动生成的查询 SQL 都会自动追加AND deleted 0条件。ListUserusersuserDao.selectList(null);// SQL: SELECT id,name,... FROM tbl_user WHERE deleted0被标记为已删除的记录将不会出现在查询结果中。如果需要查询全部数据包括已删除需要自定义 SQL 绕过 MP 的自动条件。四、乐观锁1 并发问题背景并发问题在秒杀、抢购等业务中尤为突出。例如库存只有 1 件多个用户同时下单若不加以控制会出现超卖负库存。乐观锁正是应对此类并发的一种策略适用于并发冲突不太频繁的场景。2 乐观锁实现步骤乐观锁的思想给数据增加一个“版本号”字段每次更新时检查版本号。若版本号与读取时一致则更新并将版本号加一若不一致则说明数据已被他人修改本次更新失败。实现流程用户B数据库用户A用户B数据库用户A查询 id3, version3查询 id3, version3update nameJock aaa where id3 and version3 (成功version变为4)update nameJock bbb where id3 and version3 (失败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(){MybatisPlusInterceptorinterceptornewMybatisPlusInterceptor();// 分页拦截器interceptor.addInnerInterceptor(newPaginationInnerInterceptor());// 乐观锁拦截器interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());returninterceptor;}}OptimisticLockerInnerInterceptor负责在 update 语句中自动拼接where version ?并将version加 1。3 ) 乐观锁使用示例修改数据时必须保证实体中携带version值通常做法是先查询再修改TestvoidtestUpdateWithLock(){// 1. 先查询UseruseruserDao.selectById(3L);// version3// 2. 修改属性user.setName(Jock888);// 3. 更新userDao.updateById(user);}执行 SQLUPDATEtbl_userSETnameJock888,version4WHEREid3ANDversion3若另一个人抢先修改了该记录version 变为 4则上述WHERE version3不成立更新行数为 0不会覆盖他人修改。4 ) 并发修改模拟编写测试代码模拟两个用户并发修改同一条数据TestvoidtestConcurrentUpdate(){// 模拟两个用户同时读取UseruserAuserDao.selectById(3L);// version3UseruserBuserDao.selectById(3L);// version3// B 先更新userB.setName(Jock aaa);userDao.updateById(userB);// 成功version 变为 4// A 后更新userA.setName(Jock bbb);userDao.updateById(userA);// 失败因为 version3 不再匹配}运行后数据库记录为Jock aaa说明 B 的更新成功A 的更新因版本过期而被忽略有效防止了数据覆盖。总结本文以实战为导向全面介绍了 MyBatis-Plus 在增删改操作中的高级特性模块核心注解/配置作用ID生成TableId(type...)灵活控制主键生成策略多数据操作deleteBatchIds / selectBatchIds批量处理数据逻辑删除TableLogic或全局配置logic-delete-field标记删除保留数据乐观锁Version OptimisticLockerInnerInterceptor解决并发更新冲突掌握这些技巧能让你在面对复杂业务需求时从容应对写出更健壮、更易维护的代码。项目完整源码可参考前面给出的目录结构各文件均提供了可直接运行的完整内容。