MyBatis-Plus-01笔记(一)

MyBatis-Plus-01笔记(一)

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

支持的数据库:任何能使用 mybatis 进行 crud, 并且支持标准 sql 的数据库

Hello MyBatis-Plus

基于 SpringBoot 项目,数据库使用 MySql

Maven 具体以来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<dependencies>
<!-- springboot 启动项 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- mysql 连接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<!-- JDBC依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- MyBatis-Plus-01 依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>MyBatis-Plus-01-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
</dependencies>

所链接库表结构如下

目录结构

application.yaml 配置文件配置信息如下:

1
2
3
4
5
6
7
8
9
10
11
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: xxxxxxx
url: jdbc:mysql://localhost:3306/stu?serverTimezone=GMT%2B8

# 配置日志
MyBatis-Plus-01:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

开启 Mapper 文件扫描

1
2
3
4
5
6
7
@SpringBootApplication
@MapperScan("cn.hznu.mapper")
public class ConnMysqlApplication {
public static void main(String[] args) {
SpringApplication.run(ConnMysqlApplication.class, args);
}
}

编写 ROM 数据库映射实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data    // Lombok生成标准函数
@AllArgsConstructor
@NoArgsConstructor
@TableName("student") // 数据库映射的表
public class Student {
@TableId("s_no") // 数据库主键映射
private String sno;

@TableField("s_name") // 非主键映射
private String sname;

@TableField("s_sex")
private String ssex;

@TableField("s_age")
private int sage;

@TableField("s_dept")
private String sdept;
}

创建 mapper 文件

1
2
3
4
@Repository    // 声明说一个持久层类,并在 spring 容器中创建对象
// 继承 BaseMapper 接口,类似 JPA,以完成最简单 CRUD 操作
public interface StudentMapper extends BaseMapper<Student> {
}

测试类

1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentMapperTest {
@Autowired
private StudentMapper studentMapper;

@Test
public void testSelect() {
studentMapper.selectList(null).forEach(System.out::println);
}
}

输出数据库数据及完成操作

主键生成策略

分布式系统唯一 Id 生成方案汇总

系统唯一 Id 是在设计一个系统的时候常常会遇见的问题,生成 Id 的方法有很多,适应不同的场景、需求以及性能要求

MyBatis-Plus-01 中默认使用雪花算法生成唯一 Id

雪花算法:snowflake 是 Twitter 开源的分布式 Id 生成算法,结果是一个 long 型的 Id

具体构成:使用41bit作为毫秒数10bit作为机器的Id(5个bit是数据中心,5个bit的机器Id),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 Id),最后还有一个符号位,永远是0

设置插入输入时自增 Id 的策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 在主键上设置 type,属性,MyBatis-Plus-01 已将相应策略封装成枚举类型 IdType
@TableId(value = "s_no", type = IdType.ASSIGN_ID)

/**
* 生成ID类型枚举类
*/
@Getter
public enum IdType {
/**
* 数据库ID自增
* <p>该类型请确保数据库设置了 ID自增 否则无效</p>
*/
AUTO(0),
/**
* 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
*/
NONE(1),
/**
* 用户输入ID
* <p>该类型可以通过自己注册自动填充插件进行填充</p>
*/
INPUT(2),

/* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 分配ID (主键类型为number或string),
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
*
* @since 3.3.0
*/
ASSIGN_ID(3),
/**
* 分配UUID (主键类型为 string)
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
*/
ASSIGN_UUID(4),
}

插入操作

1
2
3
4
5
6
7
8
9
10
@Test
public void testInsert() {
Student student = new Student();
student.setSname("竹青");
student.setSsex("0");
student.setSage(18);
student.setSdept("CS");

studentMapper.insert(student);
}

更新操作

1
2
3
4
5
6
7
8
@Test
public void testUpdate() {
Student student = new Student();
student.setSno("95001");
student.setSdept("MATH");

studentMapper.updateById(student);
}

自动填充

对于数据表的创建时间(create_time)修改时间(update_time)一般都是自动化完成的

数据库级别

首先为表添加两个字段,数据库中为 create_timeupdate_time,类型为 TimeStamp,默认值为 CURRENT_TIMESTAMP,对于update_time 选择根据当前时间戳更新

在 pojo 实体类中

1
2
3
4
5
@TableField("create_time")
private LocalDateTime createTime;

@TableField("update_time")
private LocalDateTime updateTime;

执行上述更新操作,发现时间自动填充

代码级别

实际开发中更倾向于代码级别的操作

  1. 先将上述 数据库级别 操作的设置默认值和自更新设置去除

  2. 在 pojo 中设置自动填充策略

    1
    2
    3
    4
    5
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    具体填充策略分别如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 字段填充策略枚举类
    public enum FieldFill {
    // 默认不处理
    DEFAULT,
    // 插入时填充字段
    INSERT,
    // 更新时填充字段
    UPDATE,
    // 插入和更新时填充字段
    INSERT_UPDATE
    }
  3. 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Slf4j
    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
    log.info("start insert fill ....");
    // 插入数据时字段都更新
    this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
    this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
    log.info("start update fill ....");
    // 更新操作时,只有更新字段更新
    this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
    }
  4. 使用上述插入和更新操作进行测试

乐观锁/悲观锁

乐观锁

顾名思义,十分乐观,无论干什么都不去上锁

乐观锁实现方式:

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

测试 MyBatis-Plus-01 的乐观锁插件

  1. 给表加上一个 version 字段,支持的数据类型只有 int, Integer, long, Long, Date, Timestamp, LocalDateTime

  2. 同时在实体类中添加相应字段,并添加相应注解 @Version 表明该字段是乐观锁

  3. 注册组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 自动管理事务,默认开启
    @EnableTransactionManagement
    @Configuration
    @MapperScan("cn.hznu.mapper")
    public class MyBatisPlusConfig {

    // 注册乐观锁插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
    }
    }
  4. 测试

    具体的操作是先取出当前的 version ,再在执行更新或其他将 version 值传入后,再执行更新操作

    • 测试乐观锁成功测试案例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 测试乐观锁成功
      @Test
      public void testOptimisticLockerSucc() {
      // 查询用户信息
      Student student = studentMapper.selectById("95001");
      // 修改用户信息
      student.setSdept("CS");
      int update = studentMapper.updateById(student);
      }

      疑问,为什么要自己手动获取或传入 version,直接在执行更新操作前内部取出相应 version 不是更合理吗,有的想不通。

      上述测试案例还会导致一个问题——updateTime 字段自动填充失败

      具体原因:在 updateById 方法中所传的实体参数,针对自动填充的字段

      • 如果字段值非空,则按照所传的值更新
      • 如果字段值为空,则按照自动填充的规则更新

      从源码来看是因为编写填充策略时具体调用等方法有关

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Slf4j
      @Component
      public class MyMetaObjectHandler implements MetaObjectHandler {
      @Override
      public void updateFill(MetaObject metaObject) {
      log.info("start update fill ....");
      this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
      // 或者
      this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
      // 或者
      this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
      }
      }

      对于官网推荐的三种写法 strictUpdateFill, strictUpdateFill, fillStrategy

      strictFillStrategy 只有非空才能自动填充

      而如果使用 setFieldValByName 的话,不需要判断该属性是否为空,直接覆盖

      而在上述案例中,传入的 student 对象的 update_time 属性是非空的,故直接赋值了过去而并未更新时间

      解决方案:

      • 最简单的就是将要修改的值封装成一个新对象,然后对于时间等需要自动填充属性设置为空即可

      • 编写具体填充策略实现时选择 setFieldValByName 的写法

        1
        2
        3
        4
        5
        6
        @Override
        public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        //this.strictUpdateFill(metaObject, "gmtUpdate", LocalDateTime.class, LocalDateTime.now());
        this.setFieldValByName("gmtUpdate", LocalDateTime.now(), metaObject);
        }

        然后使用时正常调用即可

    • 测试乐观锁失败测试案例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      // 测试乐观锁失败
      @Test
      public void testOptimisticLockerFail() {
      // 模拟多线程操作
      // 查询用户1信息
      Student student1 = studentMapper.selectById("95001");

      // 模拟多线程,线程插队现象
      // 查询用户2信息
      Student student2 = studentMapper.selectById("95001");
      // 修改用户2信息
      student2.setSdept("IS");
      studentMapper.updateById(student2);

      // 修改用户1信息
      student1.setSdept("MA");
      studentMapper.updateById(student1);
      }

      最终 sdept 值为 IS ,对 student1 进行更新时 version 值错乱,导致更新失败

悲观锁

与乐观锁相反,无论干什么都要上锁

分页查询

MyBatis-Plus-01 内置分页插件,具体使用步骤如下

  1. 注册分页插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 自动管理事务,默认开启
    @EnableTransactionManagement
    @Configuration
    @MapperScan("cn.hznu.mapper")
    public class MyBatisPlusConfig {
    // 注册插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

    // 注册乐观锁插件
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

    // 注册分页插件
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
    }
    }
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testPageHelper() {
    Page<Student> objectPage = new Page<>(1, 4);

    studentMapper.selectPage(objectPage, null);

    List<Student> records = objectPage.getRecords();
    records.forEach(System.out::println);
    }

逻辑删除

即软删除,增加一个字段作为当前行是否存在的标记

使用步骤

  1. 在表中增加一个 deleted 字段,可以直接设定一个默认值为 1代表该条数据存在,或者在自动填充的的 handle实现类中声明

    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    public void insertFill(MetaObject metaObject) {
    log.info("start insert fill ....");
    this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
    this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    // 自动填充软删除默认状态
    this.strictInsertFill(metaObject, "deleted", Integer.class, 1);
    }
  2. 在实体类中声明该字段的映射,并添加注解 @TableLogic

  3. 配置配置文件 application.yaml

    1
    2
    3
    4
    5
    6
    # 配置软删除
    global-config:
    db-config:
    logic-delete-field: delete
    logic-delete-value: 1 # 逻辑已删除值(默认为 1)
    logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
  4. 测试

执行 SQL 分析打印

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

具体使用步骤如下

  1. 导入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
    </dependency>
  2. 修改 application.yaml 配置文件

    1
    2
    3
    4
    5
    6
    spring:
    datasource:
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    username: root
    password: xxxxxxxx
    url: jdbc:p6spy:mysql://localhost:3306/stu

    更换数据哭驱动,且在 url 上添加 p6spy

  3. 创建配置文件 spy.properties,具体配置信息如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #3.2.1以上使用
    modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
    #3.2.1以下使用或者不配置
    #modulelist=com.p6spy.engine.logging.P6LogFactory,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=org.h2.Driver
    # 是否开启慢SQL记录
    outagedetection=true
    # 慢SQL记录标准 2 秒
    outagedetectioninterval=2
  4. 测试即可得到分析结果打印

条件构造器

具体参考官方文档

代码生成器

用于自动生成模版代码

具体使用步骤

  1. 导入依赖,对于 MyBatis-Plus-01-generator,官网使用的是 3.4.2 版本,但是导入失败,

    查了一下 Maven,好像还没上传,最新职业 3.4.1 版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>MyBatis-Plus-01-generator</artifactId>
    <version>3.4.1</version>
    </dependency>

    <dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.2</version>
    </dependency>
  2. 编写代码生成器代码,可以直接参照官网,以下是我自己的模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    // 代码自动生成器
    public class CodeGenerator {
    public static void main(String[] args) {
    // 构建 代码自动生成器 对象
    AutoGenerator autoGenerator = new AutoGenerator();

    // 配置策略

    // 1. 全局配置
    GlobalConfig globalConfig = new GlobalConfig();
    // 输出路径
    String projectPath = System.getProperty("user.dir");
    globalConfig.setOutputDir(projectPath + "/code-generator/src/main/java");
    // 输出作者
    globalConfig.setAuthor("秋寒");
    // 是否打开输出目录
    globalConfig.setOpen(false);
    // 是否覆盖已有文件
    globalConfig.setFileOverride(false);
    // 设置 service 文件名称方式
    globalConfig.setServiceName("%sService");
    // 设置主键生成策略
    globalConfig.setIdType(IdType.ASSIGN_ID);
    // 设置日期类型
    globalConfig.setDateType(DateType.ONLY_DATE);
    // 设置开启 swagger2 模式
    globalConfig.setSwagger2(true);
    // 将全局配置放入 代码生成器 对象中
    autoGenerator.setGlobalConfig(globalConfig);

    // 2. 设置数据源
    DataSourceConfig dataSourceConfig = new DataSourceConfig();
    dataSourceConfig.setDbType(DbType.MYSQL);
    dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
    dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/stu?serverTimezone=GMT%2B8");
    dataSourceConfig.setUsername("root");
    dataSourceConfig.setPassword("xxxxx");
    // 将数据库配置放入代码生成器中
    autoGenerator.setDataSource(dataSourceConfig);

    // 3. 包的配置
    PackageConfig packageConfig = new PackageConfig();
    // packageConfig.setModuleName("code-generator");
    packageConfig.setParent("cn.hznu");
    packageConfig.setEntity("pojo");
    packageConfig.setMapper("mapper");
    packageConfig.setService("service");
    packageConfig.setController("controller");
    // 将包配置放入代码生成器中
    autoGenerator.setPackageInfo(packageConfig);

    // 4. 策略配置
    StrategyConfig strategy = new StrategyConfig();
    // 设置命名格式为 下划线转驼峰
    strategy.setNaming(NamingStrategy.underline_to_camel);
    strategy.setColumnNaming(NamingStrategy.underline_to_camel);
    // 自动使用 Lombok
    strategy.setEntityLombokModel(true);
    // 设置要映射的表名
    strategy.setInclude("student"); // 可变参数,多表直接夹在后面即可
    // 设置软删除字段名
    strategy.setLogicDeleteFieldName("deleted");

    // 设置自动填充策略
    TableFill createTime = new TableFill("create_time", FieldFill.INSERT);
    TableFill updateTime = new TableFill("update_time", FieldFill.INSERT_UPDATE);
    // 将填充策略放入策略配置中
    strategy.setTableFillList(Arrays.asList(createTime, updateTime));

    // 设置乐观锁
    strategy.setVersionFieldName("version");
    // 开启 Rest 风格的驼峰命名
    strategy.setRestControllerStyle(true);
    // 设置请求链接下划线命名
    strategy.setControllerMappingHyphenStyle(true);

    // 将策略配置放入代码生成器中
    autoGenerator.setStrategy(strategy);

    // 生成
    autoGenerator.execute();
    }
    }
  3. 观察结果,已在制定目录生成成功

总计:很快捷的开发工具