Mybatis-Plus实战

Mybatis-Plus官网:https://www.baomidou.com/

源码地址:gitee https://gitee.com/baomidou/mybatis-plus

​ github https://github.com/baomidou/mybatis-plus

#简介

MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

#特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

#支持数据库

任何能使用 MyBatis 进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下,如果不在下列表查看分页部分教程 PR 您的支持。

  • MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb,informix,TDengine,redshift
  • 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库,优炫数据库

总结

总的来说,Mybatis-Plus 大大提高了框架的开发效率,让开发人员能够更加快速、方便地实现业务开发。当然,Mybatis-Plus 也有它的缺点,例如对于复杂查询和多表查询的支持不够完善等。不过,随着 Mybatis-Plus 的不断发展,相信它在后续的版本中会不断完善和优化,成为更加优秀的 ORM 框架。

如果你是 Mybatis 的使用者,那么可以尝试使用Mybatis-Plus,这将大大简化你的代码开发流程,提高你的开发效率。

1、Mybatis-Plus常用注解

注解用途描述
@TableName定义数据表名为实体类定义数据库表名,可以设置驼峰转下划线等自动转换方式
@TableId定义主键ID定义主键字段,可以设置主键类型、主键生成策略、是否自增等
@TableField定义数据库列名可以设置列名、是否为主键、是否为自动填充等
@Version数据版本控制定义版本属性,实现乐观锁功能
@EnumValue枚举属性与数据库值的映射用于确定枚举属性的值和相应在数据库中的值的映射关系
@TableLogic逻辑删除标识定义逻辑删除属性,用于标识数据是否被删除

2、条件构造器

2.1 介绍

Mybatis-Plus提供了强大的条件构造器。这里简要介绍了条件构造器的结构与关系,并通过简要的示例描述查询、删除和修改操作的条件构造器实现方式。

在这里插入图片描述

Wrapper : 条件构造抽象类,最顶端父类
AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
QueryWrapper : Entity 对象封装操作类,不是用lambda语法
UpdateWrapper : Update 条件封装,用于Entity对象更新操作
AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取column。
LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper
LambdaUpdateWrapper : Lambda 更新封装Wrapper

2.2 条件

方法符号示例说明
eq=eq("age", 20)等于
ne!=ne("age", 20)不等于
gt>gt("age", 18)大于
ge>=ge("age", 18)大于等于
lt<lt("age", 25)小于
le<=le("age", 25)小于等于
betweenBETWEEN … AND …between("age", 18, 25)在指定范围之间
notBetweenNOT BETWEEN … AND …notBetween("age", 18, 25)不在指定范围之间
likeLIKE`like(“name”, “王”)模糊查询
notLikeNOT LIKEnotLike("name", "王")不模糊查询
inINin("age", Arrays.asList(18, 20, 22))包含某些值
notInNOT INnotIn("age", Arrays.asList(18, 20, 22))不包含某些值
isNullIS NULLisNull("name")为空
isNotNullIS NOT NULLisNotNull("name")不为空
orderByAscASCorderByAsc("age")升序排序
orderByDescDESCorderByDesc("age")降序排序

3、Mybatis-Plus属性配置

3.1 Mybatis-Plus常用配置项

配置项描述默认值取值范围
spring.datasource.url数据库连接字符串数据库的连接字符串
spring.datasource.username数据库账号数据库账号
spring.datasource.password数据库密码数据库密码
spring.datasource.driver-class-name数据库驱动类名数据库驱动类名
mybatis-plus.mapper-locationsMybatis-Plus映射文件位置Mybatis-Plus映射文件所在位置
mybatis-plus.global-config.db-config.page-size每页显示的记录数10大于0的整数
mybatis-plus.global-config.db-config.page-helper是否开启分页插件truetrue或false
mybatis-plus.global-config.db-config.logic-delete-value逻辑删除时的值1自定义类型
mybatis-plus.global-config.db-config.logic-not-delete-value逻辑未删除时的值0自定义类型
mybatis-plus.global-config.db-config.id-type主键生成策略auto自增主键、UUID主键、雪花算法主键、全局唯一ID主键、用户自定义主键类型
mybatis-plus.global-config.db-config.field-strategy表字段处理策略ignoreinclude或exclude
mybatis-plus.global-config.db-config.optimistic-lock-field乐观锁字段名表的非主键字段名
mybatis-plus.db-config.table-prefix数据库表前缀表前缀
mybatis-plus.db-config.capital-mode数据库是否大小写敏感truetrue或false

3.2 mybatis-plus.configuration. 下配置项

配置项描述默认值取值范围
map-underscore-to-camel-case是否开启字段下划线转驼峰falsetrue/false
cache-enabled是否开启二级缓存falsetrue/false
lazy-loading-enabled是否开启延迟加载falsetrue/false
aggressive-lazy-loading是否开启懒加载,当开启时,会影响所有延迟加载设置falsetrue/false
multiple-result-sets-enabled是否开启使用列标签代替列名truetrue/false
use-column-label是否允许一次返回多个结果集,需要驱动支持falsetrue/false
use-generated-keys是否允许JDBC支持自动生成主键falsetrue/false
default-statement-timeout设置超时时间,单位为秒,可以被单独的 statement 覆盖整数
default-fetch-size设置 JDBC 数据库查询时每次返回的最大结果集条数,可以被单独的 statement 覆盖整数
default-statement-type设置默认的statement类型,可选值为:STATEMENT, PREPARED, CALLABLEPREPAREDSTATEMENT/PREPARED/CALLABLE
default-executor-type设置默认的 Executor 类型,可选值为:SIMPLE, REUSE, BATCHSIMPLESIMPLE/REUSE/BATCH
safe-row-bounds-enabled是否允许在嵌套语句中使用分页(RowBounds)falsetrue/false
use-aggressive-mappings是否启用行为null时的字段映射,即不为空时才映射该字段falsetrue/false
call-setters-on-nulls当 null 值被手动设置到 SQL 参数时是否调用 Setter 方法falsetrue/false
return-instance-for-empty-row当返回值为空时,是否返回 null 值,而不是一个新的实例对象falsetrue/false
log-prefix日志前缀字符串
configuration-factory返回自定义配置的 ConfigurationFactory字符串
log-impl日志mybatis-plus SQL 打印org.apache.ibatis.logging.stdout.StdOutImpl

希望以上内容对您有所帮助,如何不足请谅解。

3.3 配置小结

#mybatis-plus
mybatis-plus.mapper-locations=classpath*:**/mapper/xml/*.xml
mybatis-plus.type-aliases-package=com.caochenlei.mpdemo.pojo
mybatis-plus.type-handlers-package=com.caochenlei.mpdemo.type
mybatis-plus.type-enums-package=com.caochenlei.mpdemo.enum
mybatis-plus.check-config-location=false
mybatis-plus.executor-type=simple
#mybatis-plus.configuration
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.default-enum-type-handler=org.apache.ibatis.type.EnumTypeHandler
mybatis-plus.configuration.aggressive-lazy-loading=true
mybatis-plus.configuration.lazy-loading-enabled=true
mybatis-plus.configuration.auto-mapping-behavior=partial
mybatis-plus.configuration.auto-mapping-unknown-column-behavior=none
mybatis-plus.configuration.local-cache-scope=session
mybatis-plus.configuration.cache-enabled=true
mybatis-plus.configuration.call-setters-on-nulls=false
mybatis-plus.configuration.configuration-factory=
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#mybatis-plus.global-config
mybatis-plus.global-config.banner=true
mybatis-plus.global-config.enable-sql-runner=false
mybatis-plus.global-config.super-mapper-class=com.baomidou.mybatisplus.core.mapper.Mapper
#mybatis-plus.global-config.db-config
mybatis-plus.global-config.db-config.id-type=assign_id
mybatis-plus.global-config.db-config.table-prefix=tbl_
mybatis-plus.global-config.db-config.schema=
mybatis-plus.global-config.db-config.column-format=
mybatis-plus.global-config.db-config.property-format=
mybatis-plus.global-config.db-config.table-underline=true
mybatis-plus.global-config.db-config.capital-mode=false
mybatis-plus.global-config.db-config.logic-delete-field=
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0
mybatis-plus.global-config.db-config.insert-strategy=not_null
mybatis-plus.global-config.db-config.update-strategy=not_null
mybatis-plus.global-config.db-config.select-strategy=not_null

4、CRUD实战

CRUD简单实现

1、根据我上一张的文章进行SpringBoot项目搭建 :https://blog.csdn.net/ITKidKid/article/details/130863898

4.1 创建数据库

# 该 SQL 命令用于删除名为 testdemo 的数据库。如果该数据库不存在,则不执行任何操作。它通常用于删除整个数据库,以便重新创建或者清空数据库。
DROP DATABASE IF EXISTS testdemo; 
# 该 SQL 命令选择 testdemo 数据库,并把它设为当前正在使用的数据库,以便接下来的操作默认在该数据库中执行。
USE testdemo;
# 这是一个用于删除名为 "test_demo" 的数据库表的 SQL 命令。它先检查该表是否存在,如果存在就删除它。通常用于重新创建一个表。
DROP TABLE IF EXISTS test_demo; 
# 创建表
CREATE TABLE test_demo (
   id INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
   username VARCHAR(50) NOT NULL COMMENT '用户名',
   password VARCHAR(50) NOT NULL COMMENT '密码',
   email VARCHAR(50) NOT NULL COMMENT '邮箱',
   mobile CHAR(11) NOT NULL COMMENT '手机号',
   create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (id),
   UNIQUE KEY (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账号信息表';
# 新增数据
INSERT INTO test_demo (username, password, email, mobile, create_time, update_time) VALUES 
   ('user1', 'password1', 'user1@example.com', '13888888888', NOW(), NOW()),
   ('user2', 'password2', 'user2@example.com', '13999999999', NOW(), NOW()),
   ('user3', 'password3', 'user3@example.com', '13111111111', NOW(), NOW()),
   ('user4', 'password4', 'user4@example.com', '13222222222', NOW(), NOW()),
   ('user5', 'password5', 'user5@example.com', '13333333333', NOW(), NOW());

4.2 代码生成器

生成相关的结构代码:https://blog.csdn.net/ITKidKid/article/details/126185295

这些操作执行完了,先把代码生成的依赖给去除,后续还需要使用的话也可以不用删除,

4.3 启动类添加注解

启动类添加注解 @MapperScan,@MapperScan是MyBatis框架中的一个注解,用于扫描Mapper接口的注解,将Mapper接口和SQL语句进行绑定。它的作用是在启动项目时自动扫描包中的所有Mapper接口,并生成对应的代理实现类,并将它们注册到Spring容器中,方便在其他组件中进行依赖注入。

@MapperScan("com.example.demo.mapper")

4.4 添加依赖

如果有就没必要重复添加了

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.2</version>
    </dependency>
    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.29</version>
    </dependency>
    <!--lombok依赖-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
    </dependency>

4.5 application.yml配置

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/testdemo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: 数据库账号
    password: 数据库密码
    hikari:
      # 连接池名
      pool-name: DataHikariCP
      # 最小空闲连接数
      minimum-idle: 5
      # 空闲连接存活最大时间,默认6000010分钟)
      idle-timeout: 180000
      # 最大连接数,默认10
      maximum-pool-size: 10
      # 从连接池返回的连接的自动提交
      auto-commit: true
      # 连接最大存活时间,0表示永久存活,默认180000030分钟)
      max-lifetime: 1800000
      # 连接超时时间,默认3000030秒)
      connection-timeout: 30000
      # 测试连接是否可用的查询语句
      connection-init-sql: SELECT 1

# mybatis-plus配置
mybatis-plus:
  configuration:
    # 是否关闭驼峰转换
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
    # 日志mybatis-plus SQL 打印
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

4.6 控制层代码实现

普通增删改查实现

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author zzw
 * @since 2023-05-26
 */
@RestController
@RequestMapping("/testDemo")
public class TestDemoController {
    @Resource
    private TestDemoService testDemoService;

    @GetMapping("/list")
    Map<String,Object> list() {
        HashMap<String,Object> map = new HashMap<>();
        List<TestDemo> list = testDemoService.list(null); // 无查询条件
        LambdaQueryWrapper<TestDemo> queryWrapper = new LambdaQueryWrapper<>();
        // queryWrapper.eq(条件,参数,参数值);
        queryWrapper.between(1==1, TestDemo::getCreateTime,"2023-05-31 00:00:00","2023-05-31 23:59:59")
                .eq(1!=1, TestDemo::getUsername,"user1")
                .like(1==1, TestDemo::getMobile,"133");
        List<TestDemo> list1 = testDemoService.list(queryWrapper); // 使用条件构造器查询
        map.put("code",200);
        map.put("message","查询成功");
        map.put("list",list);
        map.put("list1",list1);
        return map;
    }

    @DeleteMapping("/delete")
    Map<String,Object> delete(@RequestParam Integer id) {
        HashMap<String,Object> map = new HashMap<>();
        boolean remove = testDemoService.removeById(id);
        if (remove){
            map.put("code",200);
            map.put("message","删除成功");
        }else {
            map.put("code",500);
            map.put("message","删除失败");
        }
        return map;
    }

    @PutMapping(value = "/update")
    Map<String,Object> update(@RequestBody TestDemo testDemo) {
        HashMap<String,Object> map = new HashMap<>();
        boolean update = testDemoService.updateById(testDemo);
        if (update){
            map.put("code",200);
            map.put("message","修改成功");
        }else {
            map.put("code",500);
            map.put("message","修改失败");
        }
        return map;
    }

    @PostMapping("/insert")
    Map<String,Object> insert() {
        HashMap<String,Object> map = new HashMap<>();
        TestDemo testDemo = new TestDemo();
        testDemo.setCreateTime(LocalDateTime.now());
        testDemo.setUsername("admin1");
        testDemo.setMobile("15698756234");
        testDemo.setPassword("123456");
        testDemo.setEmail("xxx@qq.com");
        boolean save = testDemoService.save(testDemo);
        if (save){
            map.put("code",200);
            map.put("message","新增成功");
        }else {
            map.put("code",500);
            map.put("message","新增失败");
        }
        return map;
    }

}

5、插件扩展

注意: 所有的配置类编写在 com.example.demo.config 包下面

5.1 分页插件

配置类编写

@Configuration
public class MybatisPlusConfig {
    /**
     * 分页配置
     * @return 3.3.1及以下版本使用下面这个分页插件
     */
    @Bean 
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
	
	/**
     * 分页配置
     * @return 3.3.1以上版本使用下面这个分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //插件分页拦截器,我的是mysql
        return mybatisPlusInterceptor;
    }
}

代码实战

	/**
     *
     * @param currPageNo 当前页码
     * @param pageSize 每页条数
     * @return
     */
    @PostMapping("/page")
    Map<String,Object> page(Integer currPageNo,Integer pageSize) {
        HashMap<String,Object> map = new HashMap<>();
        Page<TestDemo> page = new Page<>(currPageNo, pageSize);
        Page<TestDemo> page1 = testDemoService.page(page);
        Integer total = Math.toIntExact(page1.getTotal());
        Integer current = Math.toIntExact(page1.getCurrent());
        map.put("total",total); // 总条数
        map.put("current",current); // 当前页码
        map.put("content",page1.getRecords()); // 查询出来的数据
        map.put("code",200);
        map.put("message","查看分页数据成功");
        return map;
    }

5.2 执行分析插件

插件功能:防止全表更新与全表删除,依次保护数据库的安全,建议开发环境使用,不建议生产环境使用。

添加插件

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //插件分页拦截器,我的是mysql
        mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 添加执行分析插件
        return mybatisPlusInterceptor;
    }
}

代码实战:

    @DeleteMapping("/delete")
    Map<String,Object> delete() {
        HashMap<String,Object> map = new HashMap<>();
        boolean remove = testDemoService.remove(null);
        if (remove){
            map.put("code",200);
            map.put("message","删除成功");
        }else {
            map.put("code",500);
            map.put("message","删除失败");
        }
        return map;
    }

结果:

报错:com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of full table deletion(禁止删除全表)

5.3 性能分析插件

插件功能:该功能依赖 p6spy 组件,完美的输出打印 SQL 及执行时长, 3.1.0 以上版本支持。

添加依赖

<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
</dependency>

修改配置:application.yml

spring:
  datasource:
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    url: jdbc:p6spy:mysql://localhost:3306/testdemo
    username: # 数据库账号
    password: # 数据库密码

添加配置:spy.properties

# 3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
# 日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
driverlist=com.mysql.jdbc.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2

注意问题:

driver-class-name 为 p6spy 提供的驱动类
url 前缀为 jdbc:p6spy 跟着冒号为对应数据库连接地址
打印出sql为null,在excludecategories增加commit
批量操作不打印sql,去除excludecategories中的batch
批量操作打印重复的问题请使用MybatisPlusLogFactory (3.2.1新增)
该插件有性能损耗,不建议生产环境使用

添加插件: 该插件不用添加自动集成

代码实战:

    @PostMapping("/page")
    Map<String,Object> page(Integer currPageNo,Integer pageSize) {
        HashMap<String,Object> map = new HashMap<>();
        Page<TestDemo> page = new Page<>(currPageNo, pageSize);
        Page<TestDemo> page1 = testDemoService.page(page);
        Integer total = Math.toIntExact(page1.getTotal());
        Integer current = Math.toIntExact(page1.getCurrent());
        map.put("total",total); // 总条数
        map.put("current",current); // 当前页码
        map.put("content",page1.getRecords()); // 查询出来的数据
        map.put("code",200);
        map.put("message","查看分页数据成功");
        return map;
    }

结果:

控制台打印

 Consume Time1 ms 2023-05-31 17:23:37
 Execute SQLSELECT COUNT(*) AS total FROM test_demo

 Consume Time0 ms 2023-05-31 17:23:37
 Execute SQLSELECT id,username,password,email,update_time,create_time,mobile FROM test_demo LIMIT 3,3

5.4 乐观锁插件

乐观锁: 顾名思义十分乐观,它总是认为不会出现问题,无论干什么都不去上锁!如果出现了问题,再更新值测试。
悲观锁: 顾名思义十分悲观,它总是认为会出现问题,无论干什么都会上锁,然后再去操作!

插件功能: MyBatis-Plus 乐观锁插件是 MyBatis-Plus 提供的一种并发控制手段,用于解决在多线程或分布式场景下数据库并发更新所带来的数据不一致性问题。乐观锁的原理是在数据版本号的基础上进行并发控制,每次更新时判断数据版本号是否匹配,如果不匹配则认为数据已经被其他线程更新过,这时更新操作就会失败,返回更新失败的结果。

实现方式: 乐观锁实现方式如下

取出记录时,获取当前version
更新时,带上这个version
执行更新时, set version = newVersion where version = oldVersion
如果version不对,就更新失败

乐观锁: 1、先查询,获得版本号 version=1
--A
update test_demo set username ="user1" ,version =version+1
where id =1 and version=1

--B 如果线程抢先完成,这个时候version=2,会导致A修改失败
update test_demo set username ="user1" ,version =version+1
where id =1 and version=1

1、数据库添加字段:

ALTER TABLE `test_demo` ADD COLUMN `version` INT(11) DEFAULT '1' COMMENT '版本号'; 

2、实体类添加字段:

在TestDemo类下面添加如下字段:

@Version
private Integer version;

注意事项:

支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
整数类型下 newVersion = oldVersion + 1
newVersion 会回写到 entity 中
仅支持 updateById(id) 与 update(entity, wrapper) 方法
在 update(entity, wrapper) 方法下, wrapper 不能复用

3、添加插件

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //插件分页拦截器,我的是mysql
        mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 添加执行分析插件
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 添加乐观锁插件
        return mybatisPlusInterceptor;
    }
}

4、代码实战:

单线程执行:

@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
    @Resource
    private TestDemoService testDemoService;
    @Test
    void update() {
        TestDemo testDemo = testDemoService.getById(1);
        testDemo.setUsername("testDemo1");
        testDemo.setPassword("test1");
        testDemoService.updateById(testDemo);
    }
}

控制台分析:

可以看到这里的修改条件变成了id加version了,而version的值也变成2了 WHERE id=? AND version=?

在这里插入图片描述

结果:

idusernamepasswordemailmobilecreate_timeupdate_timeversion
1testDemo1test1user1@example.com138888888882023-05-31 16:58:112023-06-01 11:07:412

可以看到版本号变成2了,说明单线程进行更新操作没有任何问题的,都会执行更新操作,那我们再来试试多线程操作,这里我们简单模拟多线程操作

多线程执行:

    @Test
    void update() {
        // 模拟多线程操作
        // 模拟线程1
        TestDemo testDemo = testDemoService.getById(1);
        testDemo.setUsername("testDemo2");
        testDemo.setPassword("test2");

        // 模拟线程2插队
        TestDemo testDemo1 = testDemoService.getById(1);
        testDemo1.setUsername("testDemo3");
        testDemo1.setPassword("test3");
        testDemoService.updateById(testDemo1); // 线程2抢先提交,这时他的版本发生了变化
        testDemoService.updateById(testDemo); // 线程1失败,乐观锁在这种情况下防止了脏数据存在,没有乐观锁就会有覆盖掉线程2的操作
    }

控制台分析:

可以看到这里的修改条件变成了id加version了,而version的值也变成3了 WHERE id=? AND version=?

线程2执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dq83QGWE-1685706938525)(E:\PRD\Images\image-20230601115424504.png)]

线程1执行结果:

线程1查询的版本号还是2,但其实版本号为2在线程2执行时就更新了变成了 version=3,所以这里查询不到,更新失败

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YMIVH4iH-1685706938525)(E:\PRD\Images\image-20230601115759720.png)]

结果:

idusernamepasswordemailmobilecreate_timeupdate_timeversion
1testDemo3test3user1@example.com138888888882023-05-31 16:58:112023-06-01 11:52:353

5.5 快速开发插件

插件功能: 可以通过接口方法名直接创建对应mapper中的sql标签,还可以Mapper和xml可以来回跳,更多功能自行探索!

计划支持:

连接数据源之后 xml 里自动提示字段
sql 增删改查
集成 MP 代码生成
其它
安装方法:

需要MybatisX插件支持,只需要安装一下就可以了,打开 IDEA,进入 File -> Settings -> Plugins,安装完成后重启

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qBkqGhaT-1685706938526)(E:\PRD\Images\image-20230601134756552.png)]

6、MyBatis-Plus其它功能

6.1 SQL注入器

MyBatis-Plus 提供了 SQL 注入器来能够自定义 MySQL 的 SQL,可以灵活的进行条件组装,从而实现动态SQL查询的目的。

1、定义接口方法,在mapper接口添加接口方法
在 package com.example.demo.mapper; 包下的 TestDemoMapper 接口添加 deleteAll() 方法

public interface TestDemoMapper extends BaseMapper<TestDemo> {
    public void deleteAll();
}

注意:我们不需要编写xml映射,因为我们会采用sql注入的形式,在 MybatisPlus 启动的时候就注入。

注:添加自定义SQL配置类和 注册自定义方法都在这个包下面 package com.example.demo.util.mp;

2、添加自定义SQL配置类
在 package com.example.demo.util.mp; 包下创建 MyMappedStatement 类,并继承 AbstractMethod,实现自定义SQL注入代码

public class MyMappedStatement extends AbstractMethod {
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        // 接口中的方法名
        String method = "deleteAll";
        // 该方法执行语句
        String sql = "delete from " + tableInfo.getTableName();
        // 创建SqlSource
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        // 构造一个删除的MappedStatement并返回
        return this.addDeleteMappedStatement(mapperClass, method, sqlSource);
    }
}

3、注册自定义方法
在 package com.example.demo.util.mp; 包下创建 MySqlInjector类,并继承 DefaultSqlInjector,实现注册SQL自定义方法

public class MySqlInjector extends DefaultSqlInjector {
    /**
     * 如果只需增加方法,保留MP自带方法
     * 可以super.getMethodList() 再add
     * @return
     */
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass,tableInfo);
        methodList.add(new MyMappedStatement());
        return methodList;
    }
}

4、添加自定义SQL注入对象

注释“执行分析插件”:会影响代码执行,它会阻止全表删除操作,所以先注释掉,反正我们已经学会了
在 package com.example.demo.config; 包下的 MybatisPlusConfig类,并注册相关 Bean,实现SQL自动注入功能

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //插件分页拦截器,我的是mysql
//        mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 添加执行分析插件
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 添加乐观锁插件
        return mybatisPlusInterceptor;
    }
    
    @Bean
    public MySqlInjector mySqlInjector() {
        return new MySqlInjector();
    }
}

5、测试
在 package com.example.testdemo; 包下的 TestDemoApplicationTests 类编写测试代码,并进行测试

@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
    @Autowired
    private TestDemoMapper testDemoMapper;

    @Test
    void testDeleteAll() {
        testDemoMapper.deleteAll();
    }
}

测试前数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FUdqi4UM-1685706938526)(E:\PRD\Images\image-20230601145555698.png)]

控制台输出:

==>  Preparing: delete from test_demo
==> Parameters: 
<==    Updates: 5

测试后数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B9CIadOH-1685706938526)(E:\PRD\Images\image-20230601164115074.png)]

6.2 逻辑删除

逻辑删除: 逻辑删除指将数据标记为“已删除”,而非真正删除。可以在表中增加一个 deleted 标志位字段,并在查询时过滤掉已被标记删除的数据。可以使用 ORM 框架的软删除插件来简化实现。

注意:在操作之前进行记得先添加数据,已进行逻辑删除的数据,使用id查询的时候默认加上了 deleted=0 的条件

1、添加application.yml配置

如果有以下配置,请忽略此步骤

com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

2、实体类字段上加上@TableLogic注解

在TestDemo类下面添加如下字段:

@TableLogic
private Integer deleted;

3、数据库添加字段

ALTER TABLE `test_demo` ADD COLUMN `deleted` INT(11) DEFAULT '0' COMMENT '是否删除(0:未删除、1:已删除)'; 

4、测试

@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
    @Resource
    private TestDemoMapper testDemoMapper;

    @Test
    void testDeleteAll() {
        testDemoMapper.deleteById(14);
    }
}

控制台输出: 可以看到我们的deleteById 的SQL变成了更新语句,并且将 deleted 字段修改成逻辑已删除值

==>  Preparing: UPDATE test_demo SET deleted=1 WHERE id=? AND deleted=0
==> Parameters: 14(Integer)
<==    Updates: 1

最终结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UVjIsA42-1685706938527)(E:\PRD\Images\image-20230602091131051.png)]

6.3 通用枚举

实现自定义枚举有两种方式,一种是使用注解而另一种是使用实现接口IEnum的方式,在接下来的案例中,我们会分别使用这两种进行讲解,这样可以让大家学的更全面一些。

注意:3.3.0之前的版本可能还需要额外的配置

1、数据库添加字段

ALTER TABLE `test_demo` ADD COLUMN (`age` INT(11),`grade` INT(11)); 

2、声明枚举

在这个目录下面 添加枚举:package com.example.demo.enums;

方式一: 使用 @EnumValue 注解枚举属性 完整示例

public enum GradeEnum {

    PRIMARY(1, "小学"),  SECONDORY(2, "中学"),  HIGH(3, "高中");

    private final String descp;

    GradeEnum(int code, String descp) {
        this.code = code;
        this.descp = descp;
    }

    @EnumValue // 标记数据库存的值是code
    private final int code;
    
}

方式二: 枚举属性,实现 IEnum 接口如下

public enum AgeEnum implements IEnum<Integer> {
    ONE(1, "一岁"),
    TWO(2, "二岁"),
    THREE(3, "三岁");

    private int value;
    private String desc;

    AgeEnum(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }

    @Override
    public Integer getValue() {
        return this.value;
    }
}

3、在实体类下面添加字段

在TestDemo类下面添加如下字段:

    /**
     * 年龄,IEnum接口的枚举处理
     * 数据库字段:age INT(3)
     */
    private AgeEnum age;


    /**
     * 年级,原生枚举(带{@link com.baomidou.mybatisplus.annotation.EnumValue}):
     * 数据库字段:grade INT(2)
     */
    private GradeEnum grade;

4、测试

@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
    @Resource
    private TestDemoService testDemoService;
    @Test
    void insert() {
        TestDemo testDemo = new TestDemo();
        testDemo.setUsername("testDemo1");
        testDemo.setPassword("test1");
        testDemo.setEmail("test1@qq.com");
        testDemo.setMobile("12345698756");
        testDemo.setCreateTime(LocalDateTime.now());
        testDemo.setAge(AgeEnum.ONE);
        testDemo.setGrade(GradeEnum.HIGH);
        testDemoService.save(testDemo);
    }
}

控制台输出:

==>  Preparing: INSERT INTO test_demo ( username, password, email, create_time, mobile, age, grade ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
==> Parameters: testDemo1(String), test1(String), test1@qq.com(String), 2023-06-02T10:30:26.042(LocalDateTime), 12345698756(String), 1(Integer), 3(Integer)
<==    Updates: 1

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YlVWxutF-1685706938527)(E:\PRD\Images\image-20230602104234905.png)]

6.4 自动填充功能

MyBatis-Plus提供了一种自动填充(AutoFill)功能,可以在执行插入或者更新操作时,自动填充指定的字段。这个功能通常用来填充一些公共字段,例如创建时间、更新时间、创建人、更新人等等,避免重复代码的编写,提高代码的可维护性。

1、添加注解

package com.example.demo.model;

将TestDemo实体类上面的更新时间和创建时间两个字段的注解修改成如下所示

    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime createTime;

2、创建自动填充功能处理器

在如下包中创建 MyMetaObjectHandler 类 package com.example.demo.util.mp;

public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        setFieldValByName("createTime", LocalDateTime.now(), metaObject);
        setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
    }
}

3、注册自动填充功能处理器:

在 package com.example.demo.config; 包下面的 MybatisPlusConfig 配置类中注册自动填充功能处理器

    @Bean
    public MyMetaObjectHandler myMetaObjectHandler() {
        return new MyMetaObjectHandler();
    }

4、测试

package com.example.testdemo;

@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
    @Resource
    private TestDemoService testDemoService;
    @Test
    void insert() { // 新增
        TestDemo testDemo = new TestDemo();
        testDemo.setUsername("testDemo1");
        testDemo.setPassword("test1");
        testDemo.setEmail("test1@qq.com");
        testDemo.setMobile("12345698756");
        testDemo.setAge(AgeEnum.ONE);
        testDemo.setGrade(GradeEnum.HIGH);
        testDemoService.save(testDemo);
    }

    @Test
    void update() { // 更新
        TestDemo testDemo = testDemoService.getById(18);
        testDemo.setUsername("testDemo2");
        testDemo.setPassword("test2");
        testDemo.setEmail("test2@qq.com");
        testDemo.setMobile("98745632102");
        testDemo.setAge(AgeEnum.TWO);
        testDemo.setGrade(GradeEnum.PRIMARY);
        testDemoService.updateById(testDemo);
    }
}

新增结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Axc9IZtb-1685706938528)(E:\PRD\Images\image-20230602110532773.png)]

更新结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y9lZWxSg-1685706938528)(E:\PRD\Images\image-20230602110641801.png)]

6.5 字段类型处理器

此版本至少在 3.2.0 开始才生效噢~

字段类型处理器,用于 JavaType 与 JdbcType 之间的转换,用于 PreparedStatement 设置参数值和从 ResultSet 或 CallableStatement 中取出一个值,本文讲解 mybaits-plus 内置常用类型处理器如何通过TableField注解快速注入到 mybatis 容器中。

  • JSON 字段类型
@Data
@Accessors(chain = true)
@TableName(autoResultMap = true)
public class User {
    private Long id;
    ...

    /**
     * 注意!! 必须开启映射注解
     *
     * @TableName(autoResultMap = true)
     *
     * 以下两种类型处理器,二选一 也可以同时存在
     *
     * 注意!!选择对应的 JSON 处理器也必须存在对应 JSON 解析依赖包
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    // @TableField(typeHandler = FastjsonTypeHandler.class)
    private OtherInfo otherInfo;

}

该注解对应了 XML 中写法为

<result column="other_info" jdbcType="VARCHAR" property="otherInfo" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler" />

6.6 自定义ID生成器

1、ID生成策略对比

策略描述实例
IdType.ASSIGN_ID自定义 ID@TableId(type = IdType.ASSIGN_ID, value = "自定义ID")
IdType.ASSIGN_UUID使用 UUID 作为 ID@TableId(type = IdType.ASSIGN_UUID)
IdType.AUTO自动增长@TableId(type = IdType.AUTO)
IdType.ID_WORKER全局唯一 ID,无前缀@TableId(type = IdType.ID_WORKER)
IdType.ID_WORKER_STR全局唯一 ID,带字符串前缀@TableId(type = IdType.ID_WORKER_STR)
IdType.INPUT手动输入@TableId(type = IdType.INPUT)
IdType.NONE不使用 ID@TableId(type = IdType.NONE)
IdType.UUID使用 UUIDHexGenerator 生成 UUID@TableId(type = IdType.UUID)

2、导入数据

# 这是一个用于删除名为 "goods" 的数据库表的 SQL 命令。它先检查该表是否存在,如果存在就删除它。通常用于重新创建一个表。
DROP TABLE IF EXISTS `goods`;
# 新建表
CREATE TABLE `goods` (
  `pid` bigint NOT NULL COMMENT '商品主键',
  `goods_name` varchar(255) DEFAULT NULL COMMENT '商品名称',
  PRIMARY KEY (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品';

3、生成代码 运行NewAutoCodeGenerator

使用代码生成器生成相关的代码:https://blog.csdn.net/ITKidKid/article/details/126185295

请输入表名,用逗号隔开:
goods

温馨提示:自3.3.0开始,默认使用雪花算法+UUID(不含中划线)

4、重写方法

方法主键生成策略主键类型说明
nextIdASSIGN_IDLong,Integer,String支持自动转换为String类型,但数值类型不支持自动转换,需精准匹配,例如返回Long,实体主键就不支持定义为Integer
nextUUIDASSIGN_UUID,UUIDString默认不含中划线的UUID生成

5、添加雪花算法工具类

在 package com.example.demo.util.mp; 包下添加 SnowflakeUtils 雪花算法工具类

/**
 * 雪花算法生成唯一的有序的序列号
 */
public class SnowFlakeUtil {

    private static SnowFlakeUtil snowFlakeUtil;
    static {
        snowFlakeUtil = new SnowFlakeUtil();
    }

    // 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
    // 1650789964886:2022-04-24 16:45:59
    private static final long INIT_EPOCH = 1650789964886L;

    // 时间位取&
    private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;

    // 记录最后使用的毫秒时间戳,主要用于判断是否同一毫秒,以及用于服务器时钟回拨判断
    private long lastTimeMillis = -1L;

    // dataCenterId占用的位数
    private static final long DATA_CENTER_ID_BITS = 5L;

    // dataCenterId占用5个比特位,最大值31
    // 0000000000000000000000000000000000000000000000000000000000011111
    private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);

    // dataCenterId
    private long dataCenterId;

    // workId占用的位数
    private static final long WORKER_ID_BITS = 5L;

    // workId占用5个比特位,最大值31
    // 0000000000000000000000000000000000000000000000000000000000011111
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);

    // workId
    private long workerId;

    // 最后12位,代表每毫秒内可产生最大序列号,即 2^12 - 1 = 4095
    private static final long SEQUENCE_BITS = 12L;

    // 掩码(最低12位为1,高位都为0),主要用于与自增后的序列号进行位与,如果值为0,则代表自增后的序列号超过了4095
    // 0000000000000000000000000000000000000000000000000000111111111111
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);

    // 同一毫秒内的最新序号,最大值可为 2^12 - 1 = 4095
    private long sequence;

    // workId位需要左移的位数 12
    private static final long WORK_ID_SHIFT = SEQUENCE_BITS;

    // dataCenterId位需要左移的位数 12+5
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    // 时间戳需要左移的位数 12+5+5
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;

    /**
     * 无参构造
     */
    public SnowFlakeUtil() {
        this(1, 1);
    }

    /**
     * 有参构造
     * @param dataCenterId
     * @param workerId
     */
    public SnowFlakeUtil(long dataCenterId, long workerId) {
        // 检查dataCenterId的合法值
        if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {
            throw new IllegalArgumentException(
                    String.format("dataCenterId 值必须大于 0 并且小于 %d", MAX_DATA_CENTER_ID));
        }
        // 检查workId的合法值
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException(String.format("workId 值必须大于 0 并且小于 %d", MAX_WORKER_ID));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }

    /**
     * 获取唯一ID
     * @return
     */
    public static Long getSnowFlakeId() {
        return snowFlakeUtil.nextId();
    }

    /**
     * 通过雪花算法生成下一个id,注意这里使用synchronized同步
     * @return 唯一id
     */
    public synchronized long nextId() {
        long currentTimeMillis = System.currentTimeMillis();
        System.out.println(currentTimeMillis);
        // 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题
        if (currentTimeMillis < lastTimeMillis) {
            throw new RuntimeException(
                    String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
                            lastTimeMillis));
        }
        if (currentTimeMillis == lastTimeMillis) {
            // 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095
            // 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095
            // 那么就使用新的时间戳
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                currentTimeMillis = getNextMillis(lastTimeMillis);
            }
        } else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095
            sequence = 0;
        }
        // 记录最后一次使用的毫秒时间戳
        lastTimeMillis = currentTimeMillis;
        // 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行
        // <<:左移运算符, 1 << 2 即将二进制的 1 扩大 2^2 倍
        // |:位或运算符, 是把某两个数中, 只要其中一个的某一位为1, 则结果的该位就为1
        // 优先级:<< > |
        return
                // 时间戳部分
                ((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)
                        // 数据中心部分
                        | (dataCenterId << DATA_CENTER_ID_SHIFT)
                        // 机器表示部分
                        | (workerId << WORK_ID_SHIFT)
                        // 序列号部分
                        | sequence;
    }

    /**
     * 获取指定时间戳的接下来的时间戳,也可以说是下一毫秒
     * @param lastTimeMillis 指定毫秒时间戳
     * @return 时间戳
     */
    private long getNextMillis(long lastTimeMillis) {
        long currentTimeMillis = System.currentTimeMillis();
        while (currentTimeMillis <= lastTimeMillis) {
            currentTimeMillis = System.currentTimeMillis();
        }
        return currentTimeMillis;
    }

    /**
     * 获取随机字符串,length=13
     * @return
     */
    public static String getRandomStr() {
        return Long.toString(getSnowFlakeId(), Character.MAX_RADIX);
    }

    /**
     * 从ID中获取时间
     * @param id 由此类生成的ID
     * @return
     */
    public static Date getTimeBySnowFlakeId(long id) {
        return new Date(((TIME_BIT & id) >> 22) + INIT_EPOCH);
    }

//    public static void main(String[] args) {
//        SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil();
//        long id = snowFlakeUtil.nextId();
//        System.out.println(id);
//        Date date = SnowFlakeUtil.getTimeBySnowFlakeId(id);
//        System.out.println(date);
//        long time = date.getTime();
//        System.out.println(time);
//        System.out.println(getRandomStr());
//
//    }

}

6、创建自定义ID生成器实体类

在 package com.example.demo.util.mp; 包下面创建 CustomIdGenerator ID生成器实体类

public class CustomIdGenerator implements IdentifierGenerator {
    @Value("${server.worker-id:1}")
    private Integer workerId;

    @Value("${server.data-center-id:1}")
    private Integer dataCenterId;

    @Override
    public Long nextId(Object entity) {
        //可以将当前传入的class全类名来作为bizKey,或者提取参数来生成bizKey进行分布式Id调用生成.
        String bizKey = entity.getClass().getName();
        //根据bizKey调用分布式ID生成
        System.out.println("bizKey:" + bizKey);
        // 获取元对象
        MetaObject metaObject = SystemMetaObject.forObject(entity);
        // 获取商品名称
        String name = (String) metaObject.getValue("goodsName");
        //上面的雪花算法
        long id = new SnowFlakeUtil(workerId, dataCenterId).nextId();
        //返回生成的id值即可.
        System.out.println("为" + name + "生成主键值->:" + id);
        return id;
    }
}				

7、注册自定义ID生成器

在 package com.example.demo.config; 包下面的 MybatisPlusConfig 注册 Bean

@Bean
public IdentifierGenerator customIdGenerator(){
    return new CustomIdGenerator();
}

8、修改主键策略

修改 package com.example.demo.model; 包下面的 Goods 类中的主键策略

    /**
     * 商品主键
     */
    @TableId(value = "pid", type = IdType.ASSIGN_ID)
    private Long pid;

9、测试

@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
    @Resource
    private GoodsService goodsService;
    @Test
    void testCustomIdGenerator() {
        Goods goods = new Goods();
        goods.setGoodsName("电脑");
        boolean save = goodsService.save(goods);
        System.out.println(save);
    }
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w7Lg9mx6-1685706938528)(E:\PRD\Images\image-20230602161128854.png)]

6.7 Sequence主键

在实际开发中,我们经常会使用到MySQL和Oracle数据库,但是这两种数据库对于主键有不同的策略,如下:

  • MySQL:支持主键自增,type = IdType.Auto
  • Oracle:支持序列自增,type = IdType.INPUT

那我们Oracle又要如何使用主键策略,在这里,我们就不进行一步一步介绍了,我们只提出解决方法

  1. 在实体类对象上添加注解@KeySequence(value=”序列名”, clazz=主键属性类型.class)

  2. 在实体类对象的主键字段上添加注解@TableId(value = “主键名称”, type = IdType.INPUT)

@KeySequence(value = "SEQ_ORACLE_INTEGER_KEY", clazz = Integer.class)
public class YourEntity {
    @TableId(value = "ID", type = IdType.INPUT)
    private Integer id;
}

在全局配置中注册com.baomidou.mybatisplus.incrementer.OracleKeyGenerator

@Bean
public IKeyGenerator keyGenerator() {
    return new oracleKeyGenerator();
}

除了Oracle,MyBatis-Plus还内置支持以下数据库序列:

  • DB2KeyGenerator
  • H2KeyGenerator
  • KingbaseKeyGenerator
  • OracleKeyGenerator
  • PostgreKeyGenerator

如果内置支持不满足你的需求,可实现IKeyGenerator接口来进行扩展。

OVER

参考文章:https://baomidou.com/
https://blog.csdn.net/qq_38490457/article/details/108809902

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值