MP3️⃣进阶②DML:增删改
1、主键生成策略
1.1、策略
1.1.1、枚举 IdType
MP 提供的枚举类
-
AUTO:自增(前提是数据表的 id 字段设置了自增)
-
NONE:无策略(跟随全局,约等于 INPUT)
-
INPUT:用户输入 ID,可自定义填充插件(通过 setter 给 entity 赋值)
-
ASSIGN_ID:雪花算法(兼容数值,字符串类型)
-
ASSIGN_UUID:UUID 算法(前提是主键为字符串类型,长度大于 32)
-
过时算法:已被取代 ASSIGN_ID 和 ASSIGN_UUID 取代。@Getter public enum IdType { AUTO(0), NONE(1), INPUT(2), ASSIGN_ID(3), ASSIGN_UUID(4); private final int key; IdType(int key) { this.key = key; } }
1.1.2、说明
不同策略的区别
- NONE:约等于INPUT,需要手动设置 id 而非自动生成,容易导致主键冲突,需要做大量校验。
- AUTO:只有 1 台数据库服务器时使用,不可作为分布式 ID。
- ASSIGN_UUID
- 优点:可作为分布式 ID,能保证唯一。
- 缺点:生成 32 位字符串,太长且占用空间,不支持排序,查询性能低。
- ASSIGN_ID
- 优点:可作为分布式 ID,生成 Long 型数值,排序性能高
- 缺点:与服务器时间有关,修改系统时间可能导致主键冲突。
雪花算法
雪花算法(SnowFlake)
Twitter 官方基于 Scala 实现的算法,生成结果为 64bit 整数。
-
结构:占位符(1)、时间戳(41)、机房 ID 和机器 ID(5+5)、序号(12)
-
说明
- 占位符:最高位是符号位,用 0 表示正数。
- 时间戳:毫秒级。
- 工作机器:分别是机房 ID 和机器 ID(分布式)
- 序号:同一毫秒生成的 ID 数,从 0 开始累加。
1.2、配置方式
1.2.1、@TableId
MP 提供的
@TableId
注解,用于控制 ID 生成策略。
- 位置:标识 entity 中作为主键的属性。
- 参数:
- value:设置数据库表的主键名(通常主键命名为 id,可忽略该参数)
- type:设置主键 ID 生成策略(值为
IdType
枚举类型)
1.2.2、全局配置
mybatis-plus:
global-config:
db-config:
id-type: 枚举值
2、逻辑删除
- 物理删除:将业务数据从数据库中丢弃(UPDATE)
- 逻辑删除:用一个字段标识业务数据是否可用,不会实际删除(UPDATE)
实现逻辑删除
- 添加字段(属性)
- 配置
- 测试
2.1、添加字段
字段的命名和取值是任意的,后续配置即可。
通常为
del_flag
,取值0
或1
。
示例:若命名风格不同,注意配置映射关系。
-
数据库添加
del_flag
(默认 0) -
实体类添加
delFlag
。private Integer delFlag;
2.2、配置方式
2.2.1、@TableLogic
-
用于标识 entity 属性,作为逻辑删除的标志。
-
属性
- value:正常值(未删除)
- delval:删除值
-
示例:0 表示未删除,1 表示删除。
@TableLogic(value = "0", delval = "1") private Integer delFlag;
2.2.2、全局配置
Hint:配置的是 entity 属性名,而非数据库表的属性名。
mybatis-plus:
global-config:
db-config:
logic-delete-field: del_flag
logic-not-delete-value: 0
logic-delete-value: 1
2.3、测试
本质:将
DELETE
操作变成UPDATE
,在删除和查询时自动追加条件。
2.3.1、删
-
代码:根据 ID 删除
userDao.deleteById(1529834804407234561L);
-
对应 SQL:实际执行 UPDATE 并修改删除标志,追加判断删除标志的查询条件。
UPDATE mp_user SET del_flag=1 WHERE id=? AND del_flag=0
2.3.2、查询
-
代码:查询全部
List<User> userList = userDao.selectList(null);
-
对应 SQL:追加判断删除标志的查询条件,自动过滤掉业务上被删除的数据。
SELECT id,name,password,age,del_flag FROM mp_user WHERE del_flag=0
查询全部
- Q:如何实现查询全部记录,包括被业务上被删除的呢?
- A:采用 MyBatis 的开发方式,编写 DAO 接口和 SQL 语句即可。
3、乐观锁
3.1、说明
-
对比:
- 悲观锁:加锁线程会阻塞其它线程(互斥思想),且在多服务器下无法控制。
- 乐观锁:对比版本号,不断尝试更新(不加锁)。
-
乐观锁原理:
-
设计表时添加 version 字段,查询记录时获得
version
。 -
更新记录时,判断查出的 version 与数据库中实际 version 是否一致。
-
version 一致,则更新并修改版本号(version + 1)
-
version 不一致,说明其它线程正在使用,重复尝试直到成功更新。
UPDATE tb_user SET xxx, version = version + 1 WHERE xxx AND version = ?
-
3.1、添加字段
字段(属性)的命名是任意的
通常命名
version
,默认值1
-
数据库表
-
实体类
@Version private Integer version;
3.2、拦截器
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor() {
MybatisPlusInterceptor mpi = new MybatisPlusInterceptor();
// 乐观锁拦截器
mpi.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mpInterceptor;
}
}
3.3、测试
执行 DML 时必须设置
version
,乐观锁机制才会生效获取 version:查询 entity 对应的表,返回的 entity 中包含了
version
。
示例:将 ID 为 1 的用户(当前版本为 1)改名为 new name1
-
查询 user,此时获取了 version
-
直接向 user 中设置更新数据
-
更新记录
User user = userDao.selectById(1L); user.setName("new name1"); userDao.updateById(user);
3.4、说明
3.4.1、SQL 语句
配置了乐观锁之后,以上操作对应的 DML 语句如下:
3.4.2、多线程情况
在多线程下,每个要执行
UPDATE
语句的线程都遵循以下规则:
-
查询记录并获得
version
。 -
执行
UPDATE
操作时,MP 自动对 SQL 语句追加有关 version 的更新语句和查询条件。- 更新版本号(version +1)
- 对比 version,若不一致说明该记录已被其它线程修改。
情景假设
两个线程同时将 ID 为 1 的用户改名
-
线程 t1 和 t2 同时查询 ID 为 1 的用户(二者获得的 version 相同,假设是 2)
// t1 User u1 = userDao.selectById(1L); // t2 User u2 = userDao.selectById(1L);
-
线程 t1 和 t2 同时执行
UPDATE
,假设 t1 的请求先被 MySQL 服务器执行。-
t1 执行
UPDATE
时将 version 更新为 3。UPDATE mp_user SET name = xxx, version = 3 WHERE id = 1 AND version = 2;
-
t2 执行时,version 条件不成立,SQL 不会执行。
UPDATE mp_user SET name = xxx, version = 3 # version = 2的条件不成立 WHERE id = 1 AND version = 2;
-
小结
- 通过 version,有效避免了并发问题。
- 配置乐观锁机制后,通常会将
UPDATE
操作放在while
等循环代码块中,执行成功或达到一定失败次数后才跳出循环。