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

本文纲要

一、ID生成策略

1.1 常见ID生成场景

1.2 使用 @TableId 指定策略

1.3 IdType 枚举详解

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,目录结构如下:

dir 复制代码
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 注解,即可指定主键生成策略。

java 复制代码
// domain/User.java (局部)
package com.wb.domain;
 
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
 
@Data 
public class User {
    @TableId(type = IdType.AUTO)   // 使用数据库自增 
    private Long id;
    private String name;
    // ... 其他字段 
}

设置 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 示例

java 复制代码
User user = new User();
user.setId(666L);          // 手动指定 id 
user.setName("程序员");
user.setPassword("wb");
userDao.insert(user);

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

java 复制代码
// 测试类 testSave()
@Test 
void testSave(){
    User user = new User();
    // user.setId(667L);    // 注释后,将使用全局策略或雪花算法 
    user.setName("程序员");
    user.setPassword("wb");
    user.setAge(12);
    user.setTel("4006184000");
    userDao.insert(user);
}

ASSIGN_ID(雪花算法默认策略)

java 复制代码
@TableId(type = IdType.ASSIGN_ID)
private Long id;

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

4 ) 深入雪花算法

雪花算法(Snowflake)生成的 id 是一个 64 位二进制数(即 Java 的 long 类型)。其结构如下:
#mermaid-svg-YQYs947S0Ms9JH8L{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YQYs947S0Ms9JH8L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YQYs947S0Ms9JH8L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YQYs947S0Ms9JH8L .error-icon{fill:#552222;}#mermaid-svg-YQYs947S0Ms9JH8L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YQYs947S0Ms9JH8L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YQYs947S0Ms9JH8L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YQYs947S0Ms9JH8L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YQYs947S0Ms9JH8L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YQYs947S0Ms9JH8L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YQYs947S0Ms9JH8L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YQYs947S0Ms9JH8L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YQYs947S0Ms9JH8L .marker.cross{stroke:#333333;}#mermaid-svg-YQYs947S0Ms9JH8L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YQYs947S0Ms9JH8L p{margin:0;}#mermaid-svg-YQYs947S0Ms9JH8L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YQYs947S0Ms9JH8L .cluster-label text{fill:#333;}#mermaid-svg-YQYs947S0Ms9JH8L .cluster-label span{color:#333;}#mermaid-svg-YQYs947S0Ms9JH8L .cluster-label span p{background-color:transparent;}#mermaid-svg-YQYs947S0Ms9JH8L .label text,#mermaid-svg-YQYs947S0Ms9JH8L span{fill:#333;color:#333;}#mermaid-svg-YQYs947S0Ms9JH8L .node rect,#mermaid-svg-YQYs947S0Ms9JH8L .node circle,#mermaid-svg-YQYs947S0Ms9JH8L .node ellipse,#mermaid-svg-YQYs947S0Ms9JH8L .node polygon,#mermaid-svg-YQYs947S0Ms9JH8L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YQYs947S0Ms9JH8L .rough-node .label text,#mermaid-svg-YQYs947S0Ms9JH8L .node .label text,#mermaid-svg-YQYs947S0Ms9JH8L .image-shape .label,#mermaid-svg-YQYs947S0Ms9JH8L .icon-shape .label{text-anchor:middle;}#mermaid-svg-YQYs947S0Ms9JH8L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YQYs947S0Ms9JH8L .rough-node .label,#mermaid-svg-YQYs947S0Ms9JH8L .node .label,#mermaid-svg-YQYs947S0Ms9JH8L .image-shape .label,#mermaid-svg-YQYs947S0Ms9JH8L .icon-shape .label{text-align:center;}#mermaid-svg-YQYs947S0Ms9JH8L .node.clickable{cursor:pointer;}#mermaid-svg-YQYs947S0Ms9JH8L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YQYs947S0Ms9JH8L .arrowheadPath{fill:#333333;}#mermaid-svg-YQYs947S0Ms9JH8L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YQYs947S0Ms9JH8L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YQYs947S0Ms9JH8L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YQYs947S0Ms9JH8L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YQYs947S0Ms9JH8L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YQYs947S0Ms9JH8L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YQYs947S0Ms9JH8L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YQYs947S0Ms9JH8L .cluster text{fill:#333;}#mermaid-svg-YQYs947S0Ms9JH8L .cluster span{color:#333;}#mermaid-svg-YQYs947S0Ms9JH8L div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YQYs947S0Ms9JH8L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YQYs947S0Ms9JH8L rect.text{fill:none;stroke-width:0;}#mermaid-svg-YQYs947S0Ms9JH8L .icon-shape,#mermaid-svg-YQYs947S0Ms9JH8L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YQYs947S0Ms9JH8L .icon-shape p,#mermaid-svg-YQYs947S0Ms9JH8L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YQYs947S0Ms9JH8L .icon-shape .label rect,#mermaid-svg-YQYs947S0Ms9JH8L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YQYs947S0Ms9JH8L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YQYs947S0Ms9JH8L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YQYs947S0Ms9JH8L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 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 支持全局配置:

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

配置 table-prefix 后,User 类会自动映射到 tb_luser 表,无需再写 @TableName("tbluser")

同样,id-type 统一了所有实体的主键生成策略。

二、多数据删除与查询

1 ) 批量删除

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

java 复制代码
@Test 
void testDelete(){
    List<Long> ids = new ArrayList<>();
    ids.add(1402551342481838081L);
    ids.add(1402553134049501186L);
    ids.add(1402553619611430913L);
    int rows = userDao.deleteBatchIds(ids);
    System.out.println("删除了 " + rows + " 条数据");
}

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

2 ) 批量查询

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

java 复制代码
@Test 
void testBatchSelect(){
    List<Long> ids = new ArrayList<>();
    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 数据库添加字段

sql 复制代码
ALTER TABLE tbl_user ADD COLUMN deleted INT DEFAULT 0 COMMENT '逻辑删除(0未删,1已删)';

3.2 实体类配置

java 复制代码
// domain/User.java (局部)
import com.baomidou.mybatisplus.annotation.TableLogic;

@Data 
public class User {
   // ...其他字段 

   @TableLogic(value = "0", delval = "1")
   private Integer deleted;
}
  • value:未删除的取值(对应数据库默认 0)。
  • delval:删除后的取值(1)。

3.3 执行删除测试

java 复制代码
userDao.deleteById(2L);

控制台打印的 SQL 变为:

sql 复制代码
UPDATE tbl_user SET deleted=1 WHERE id=2 AND deleted=0 

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

3.4 全局配置逻辑删除

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

yaml 复制代码
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted 
      logic-not-delete-value: 0 
      logic-delete-value: 1 

此后所有实体类只需包含 private Integer deleted; 字段,无需添加 @TableLogic

执行删除时,MP 自动识别

yaml 复制代码
# 完整配置示例 
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver 
    url: jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC 
    username: root 
    password: root 
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 
  global-config:
    db-config:
      id-type: assign_id 
      table-prefix: tbl_
      logic-delete-field: deleted 
      logic-not-delete-value: 0 
      logic-delete-value: 1 

3.5 逻辑删除对查询的影响

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

java 复制代码
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 #mermaid-svg-ZoxmxO9rBBhRdUDj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZoxmxO9rBBhRdUDj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZoxmxO9rBBhRdUDj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZoxmxO9rBBhRdUDj .error-icon{fill:#552222;}#mermaid-svg-ZoxmxO9rBBhRdUDj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZoxmxO9rBBhRdUDj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZoxmxO9rBBhRdUDj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZoxmxO9rBBhRdUDj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZoxmxO9rBBhRdUDj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZoxmxO9rBBhRdUDj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZoxmxO9rBBhRdUDj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZoxmxO9rBBhRdUDj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZoxmxO9rBBhRdUDj .marker.cross{stroke:#333333;}#mermaid-svg-ZoxmxO9rBBhRdUDj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZoxmxO9rBBhRdUDj p{margin:0;}#mermaid-svg-ZoxmxO9rBBhRdUDj .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZoxmxO9rBBhRdUDj text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ZoxmxO9rBBhRdUDj .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZoxmxO9rBBhRdUDj .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ZoxmxO9rBBhRdUDj .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ZoxmxO9rBBhRdUDj .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ZoxmxO9rBBhRdUDj #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ZoxmxO9rBBhRdUDj .sequenceNumber{fill:white;}#mermaid-svg-ZoxmxO9rBBhRdUDj #sequencenumber{fill:#333;}#mermaid-svg-ZoxmxO9rBBhRdUDj #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ZoxmxO9rBBhRdUDj .messageText{fill:#333;stroke:none;}#mermaid-svg-ZoxmxO9rBBhRdUDj .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZoxmxO9rBBhRdUDj .labelText,#mermaid-svg-ZoxmxO9rBBhRdUDj .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ZoxmxO9rBBhRdUDj .loopText,#mermaid-svg-ZoxmxO9rBBhRdUDj .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ZoxmxO9rBBhRdUDj .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZoxmxO9rBBhRdUDj .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ZoxmxO9rBBhRdUDj .noteText,#mermaid-svg-ZoxmxO9rBBhRdUDj .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ZoxmxO9rBBhRdUDj .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZoxmxO9rBBhRdUDj .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZoxmxO9rBBhRdUDj .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZoxmxO9rBBhRdUDj .actorPopupMenu{position:absolute;}#mermaid-svg-ZoxmxO9rBBhRdUDj .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ZoxmxO9rBBhRdUDj .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZoxmxO9rBBhRdUDj .actor-man circle,#mermaid-svg-ZoxmxO9rBBhRdUDj line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ZoxmxO9rBBhRdUDj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 查询 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 字段

sql 复制代码
ALTER TABLE tbl_user ADD COLUMN version INT DEFAULT 1 COMMENT '乐观锁版本号';

实体类添加 version 属性并注解

java 复制代码
import com.baomidou.mybatisplus.annotation.Version;

@Data 
public class User {
    // ...其他字段 

    @Version 
    private Integer version;
}

拦截器配置

java 复制代码
// config/MpConfig.java 
package com.wb.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration 
public class MpConfig {
    @Bean 
    public MybatisPlusInterceptor mpInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页拦截器 
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        // 乐观锁拦截器 
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}
  • OptimisticLockerInnerInterceptor 负责在 update 语句中自动拼接 where version = ? 并将 version 加 1。

3 ) 乐观锁使用示例

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

java 复制代码
@Test 
void testUpdateWithLock(){
    // 1. 先查询 
    User user = userDao.selectById(3L);   // version=3 
    // 2. 修改属性 
    user.setName("Jock888");
    // 3. 更新 
    userDao.updateById(user);
}

执行 SQL:

sql 复制代码
UPDATE tbl_user SET name='Jock888', version=4 WHERE id=3 AND version=3 

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

4 ) 并发修改模拟

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

java 复制代码
@Test 
void testConcurrentUpdate(){
    // 模拟两个用户同时读取 
    User userA = userDao.selectById(3L);  // version=3 
    User userB = 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 解决并发更新冲突

掌握这些技巧,能让你在面对复杂业务需求时从容应对,写出更健壮、更易维护的代码。

项目完整源码可参考前面给出的目录结构,各文件均提供了可直接运行的完整内容。

相关推荐
KaMeidebaby1 小时前
卡梅德生物技术快报|噬菌体筛选:噬菌体筛选工程化优化方案:基于侵染全链条参数调控与水环境应用验证
java·开发语言·人工智能·机器学习·spark
我命由我1234510 小时前
Android Studio - Android Studio 自定义预览尺寸
android·java·ide·java-ee·android studio·android-studio·android runtime
咖啡八杯11 小时前
GoF设计模式——迭代器模式
java·后端·设计模式·迭代器模式
AIGS00111 小时前
企业AI落地的关键认知:向量空间JBoltAI的本体语义平台
java·人工智能·人工智能ai大模型应用
KaMeidebaby12 小时前
卡梅德生物技术快报|抗体亲和力成熟工业化调控新机制:差异性浆细胞增殖工艺优化思路
java·开发语言·人工智能·算法·机器学习·架构·spark
小二·12 小时前
RAG + 向量数据库实战:ChromaDB / Milvus / FAISS 选型与性能横评
数据库·milvus·faiss
矜持的左手13 小时前
电子小白的枕边书:电子学(The Art of Electronics)
数据库·restful
吴声子夜歌13 小时前
Redis 5.x——布隆过滤器
数据库·redis·缓存