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)

    1631243987800

  • 说明

    • 占位符:最高位是符号位,用 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)

实现逻辑删除

  1. 添加字段(属性)
  2. 配置
  3. 测试

2.1、添加字段

字段的命名和取值是任意的,后续配置即可。

通常为 del_flag,取值 01

示例:若命名风格不同,注意配置映射关系。

  1. 数据库添加 del_flag(默认 0)

    image-20220526224846259

  2. 实体类添加 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、说明

  1. 对比

    • 悲观锁:加锁线程会阻塞其它线程(互斥思想),且在多服务器下无法控制。
    • 乐观锁:对比版本号,不断尝试更新(不加锁)。
  2. 乐观锁原理

    1. 设计表时添加 version 字段,查询记录时获得 version

    2. 更新记录时,判断查出的 version 与数据库中实际 version 是否一致。

    3. version 一致,则更新并修改版本号(version + 1)

    4. version 不一致,说明其它线程正在使用,重复尝试直到成功更新。

      UPDATE tb_user
      SET xxx, version = version + 1
      WHERE xxx
      	AND version = ?
      

3.1、添加字段

字段(属性)的命名是任意的

通常命名 version,默认值 1

  • 数据库表

    image-20220530094747658

  • 实体类

    @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

  1. 查询 user,此时获取了 version

  2. 直接向 user 中设置更新数据

  3. 更新记录

    User user = userDao.selectById(1L);
    user.setName("new name1");
    userDao.updateById(user);
    

3.4、说明

3.4.1、SQL 语句

配置了乐观锁之后,以上操作对应的 DML 语句如下:

image-20220530101403257

3.4.2、多线程情况

在多线程下,每个要执行 UPDATE 语句的线程都遵循以下规则:

  1. 查询记录并获得 version

  2. 执行 UPDATE 操作时,MP 自动对 SQL 语句追加有关 version 的更新语句和查询条件。

    • 更新版本号(version +1)
    • 对比 version,若不一致说明该记录已被其它线程修改。

情景假设

两个线程同时将 ID 为 1 的用户改名

  1. 线程 t1 和 t2 同时查询 ID 为 1 的用户(二者获得的 version 相同,假设是 2)

    // t1
    User u1 = userDao.selectById(1L);
    // t2
    User u2 = userDao.selectById(1L);
    
  2. 线程 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 等循环代码块中,执行成功达到一定失败次数后才跳出循环。
posted @ 2022-05-22 23:07  Jaywee  阅读(55)  评论(0编辑  收藏  举报

👇