MyBatis-Plus官网 1 :https://baomidou.com/

MyBatis-Plus官网 2:https://mybatis.plus/

1. MyBatis-Plus 概述

1.1 为什么要学?

MyBatis-Plus只需简单配置即可快速进行单表 CRUD 操作,简单的 CRUD 操作不再需要我们书写。(肯定不是因为这个)

MyBatis-Plus 由国人开发,文档很详细易上手,编码符合国人习惯,并且已连续 5 年(2017、2018 、2019、2020、2021)获得”OSC 年度最受欢迎中国开源软件”殊荣。最最最重要的是官方为我们提供了自动生成代码的代码生成器…真的是太贴心了!!🤩这不就相当于是别人都把饭喂你嘴里了吗?你还有什么理由不吃呢?😂

PS:本文中,MP 是 MyBatis-Plus 的简写。

1.2 简介

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

MyBatis-Plus简介

从这张图中我们可以看出 MP 旨在成为 MyBatis 的最好搭档,而不是替换 MyBatis ,所以可以理解为 MP 是 MyBatis 的一套增强工具,它是在 MyBatis 的基础上进行开发的,我们虽然使用 MP 但是底层依然是 MyBatis 的东西,也就是说我们也可以在 MP 中写 MyBatis 的内容。

PS:使用 MP 可以节省代码的编写,尽量 不要同时 导入 MP 和 MyBatis,避免存在依赖错误。

1.3 特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求( 言外之意,简单的 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 操作智能分析阻断,也可自定义拦截规则,预防误操作

2. 快速开始

接下来将通过一个简单的 Demo 来阐述 MP 的强大功能,在此之前,假设你已经:

  • 拥有 Java 开发环境以及相应 IDE
  • 熟悉 Spring Boot
  • 熟悉 Maven

2.1 环境准备

现有一张 User 表,其表结构如下:

id name age email
1 Jone 18 test1@baomidou.com
2 Jack 20 test2@baomidou.com
3 Tom 28 test3@baomidou.com
4 Sandy 21 test4@baomidou.com
5 Billie 24 test5@baomidou.com

步骤

1、创建一个数据库 mybatis_plus

2、创建 user 表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DROP TABLE IF EXISTS `user`;

CREATE TABLE `user`
(
id BIGINT NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);

DELETE FROM `user`;

INSERT INTO `user` (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

创建User表

3、使用 IDEA 初始化一个 SpringBoot 项目 mybatis_plus,选择 Spring Web 和 Lombok 依赖进行导入。

4、导入其他依赖

由于这个 mybatis-plus-boot-starter 包含对 Mybatis 的自动装配,因此完全可以替换掉 Mybatis 的 starter 。

1
2
3
4
5
6
7
8
9
10
11
12
<!--数据库驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.31</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>

PS:注意 SpringBoot 版本要与 Mybatis-Plus 版本相对应。这里使用的 SpringBoot 版本为 2.7.12

5、编写配置文件 application.yml 连接数据库

1
2
3
4
5
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root

6、编写实体类 User.java (此处使用了 Lombok 简化代码)

1
2
3
4
5
6
7
8
9
10
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("`user`")
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}

7、编写 Mapper 包下的 UserMapper接口

1
2
public interface UserMapper extends BaseMapper<User> {
}

8、在 Spring Boot 启动类中添加 @MapperScan 注解,扫描 Mapper 文件夹:

1
2
3
4
5
6
7
8
@SpringBootApplication
// 扫描 mapper 文件夹
@MapperScan("com.muyoukule.mapper")
public class MybatisPlusApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusApplication.class, args);
}
}

2.2 标准CRUD使用

对于标准的 CRUD 功能 MP 都提供了哪些方法可以使用呢?

新增 Insert

1
2
// 插入一条记录
int insert(T t);
  • T:泛型,新增用来保存新增数据

  • int:返回值,新增成功后返回1,没有新增成功返回的是0

在测试类中进行新增操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest
class MybatisPlusApplicationTests {
@Autowired
private UserMapper userMapper;

@Test
void testSave() {
User user = new User();
// 注意:这里没有插入id
user.setName("木又枯了");
user.setAge(18);
user.setEmail("example@gmail.com");
// 受影响的行数
int i = userMapper.insert(user);
System.out.println(i);
}

}

控制台结果:

1
1

说明数据插入成功,这时候到数据库中查看,数据库表中就会添加一条数据:

插入一条数据

但是我们发现:我们明明没有设置 ID,插入时居然自动生成了 ID,而且生成的 ID 似乎有点 “ 奇怪 ”。那这个主键 ID 是如何来的?我们更想要的是主键自增,应该是 6 才对,这个是我们后面要学习的 主键生成策略,这块的这个问题,我们暂时先放放。

删除 Delete

1
int deleteById (Serializable id)
  • Serializable:参数类型
  • int:返回值类型,数据删除成功返回 1,未删除数据返回 0

思考:参数类型为什么是一个序列化类?

Serializable关系图

从这张图可以看出:

  • String 和 Number 是 Serializable 的子类
  • Number 又是 Float,Double,Integer 等类的父类
  • 能作为主键的数据类型都已经是 Serializable 的子类
  • MP 使用 Serializable 作为参数类型,就好比我们可以用 Object 接收任何数据类型一样。

在测试类中进行删除操作:

1
2
3
4
5
@Test
void testDelete() {
int i = userMapper.deleteById(1775096675078713345L);
System.out.println(i);
}

控制台结果:

1
1

说明数据删除入成功,这时候到数据库中查看,数据库表中 ID 为 1775096675078713345 的数据就会被删除:

删除一条数据

修改 Update

1
int updateById(T t);
  • T:泛型,需要修改的数据内容,注意因为是根据 ID 进行修改,所以传入的对象中需要有 ID 属性值

  • int:返回值,修改成功后返回 1,未修改数据返回 0

在测试类中进行修改操作:

1
2
3
4
5
6
7
8
9
@Test
void testUpdate() {
User user = new User();
user.setId(1L);
user.setName("Jone001");
user.setAge(20);
int i = userMapper.updateById(user);
System.out.println(i);
}

控制台结果:

1
1

说明数据修改成功,这时候到数据库中查看,发现修改成功:

修改一条数据

查询 Select

根据 ID 查询

1
T selectById (Serializable id)
  • Serializable:参数类型,主键 ID 的值
  • T:根据 ID 查询只会返回一条数据

在测试类中进行查询操作:

1
2
3
4
5
@Test
void testSelectById() {
User user = userMapper.selectById(1L);
System.out.println(user);
}
1
User(id=1, name=Jone001, age=20, email=test1@baomidou.com)

查询所有

1
List<T> selectList(Wrapper<T> queryWrapper)
  • Wrapper:用来构建条件查询的条件,目前我们没有可直接传为 Null
  • List:因为查询的是所有,所以返回的数据是一个集合

在测试类中进行查询操作:

1
2
3
4
5
@Test
void testSelectAll() {
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}

控制台输出:

1
2
3
4
5
User(id=1, name=Jone001, age=20, email=test1@baomidou.com)
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
User(id=3, name=Tom, age=28, email=test3@baomidou.com)
User(id=4, name=Sandy, age=21, email=test4@baomidou.com)
User(id=5, name=Billie, age=24, email=test5@baomidou.com)

通过以上几个简单的步骤,我们就实现了 User 表的 CRUD 功能,甚至连 XML 文件都不用编写!

上面我们只是继承了 BaseMapper 就省去所有的单表 CRUD,怎么实现的呢?

当然是 BaseMapper 接口其中已经实现了单表的 CRUD:

BaseMapper接口

因此我们自定义的 Mapper 只要实现了这个 BaseMapper,就无需自己实现单表 CRUD 了。

3. 配置日志

使用 MP 后,部分 SQL 是不可见的,我们希望知道它是如何执行的,这个时候就需要查看日志!

在配置文件中进行配置:

1
2
3
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
MP配置日志信息

配置日志后,在以后的学习中,我们可以查看日志,观察 MP 的 SQL 执行。

4. 常见注解

在刚刚的入门案例中,我们仅仅引入了依赖,继承了 BaseMapper 就能使用 MP ,非常简单。

但是问题来了: MP 如何知道我们要查询的是哪张表?表中有哪些字段呢?

大家回忆一下,UserMapper 在继承BaseMapper 的时候指定了一个泛型 User

1
2
public interface UserMapper extends BaseMapper<User> {
}

泛型中的 User 就是与数据库对应的 PO。

MP 就是根据 PO实体的信息来推断出表的信息,从而生成 SQL 的。默认情况下:

  • MP 会把 PO 实体的类名驼峰转下划线作为表名
  • MP 会把 PO 实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
  • MP 会把名为 ID 的字段作为主键

但很多情况下,默认的实现与实际场景不符,因此 MP 提供了一些注解便于我们声明表信息。

4.1 @TableName

  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类
1
2
3
4
5
6
7
@TableName("sys_user")
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}

TableName 注解除了指定表名以外,还可以指定很多其它属性:

属性 类型 必须指定 默认值 描述
value String “” 表名
schema String “” schema
keepGlobalPrefix boolean false 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时)
resultMap String “” xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定)
autoResultMap boolean false 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入)
excludeProperty String[] {} 需要排除的属性名 @since 3.3.1

4.2 @TableId

  • 描述:主键注解
  • 使用位置:实体类主键字段
1
2
3
4
5
6
7
8
@TableName("sys_user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
private String email;
}

TableId 注解支持两个属性:

属性 类型 必须指定 默认值 描述
value String “” 表名
type Enum IdType.NONE 指定主键类型

IdType 支持的类型有:

描述
AUTO 数据库 ID 自增
NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT insert 前自行 set 主键值
ASSIGN_ID 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)
ASSIGN_UUID 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)
ID_WORKER 分布式全局唯一 ID 长整型类型(please use ASSIGN_ID)
UUID 32 位 UUID 字符串(please use ASSIGN_UUID)
ID_WORKER_STR 分布式全局唯一 ID 字符串类型(please use ASSIGN_ID)

这里比较常见的有三种:

  • ASSIGN_ID:雪花算法生成Long类型的全局唯一ID,这是 MP 默认的 ID 策略
  • AUTO:利用数据库的ID自增长
  • INPUT:手动生成ID

4.2.1 主键生成策略

雪花算法

雪花算法(Snowflake)是 Twitter 开源的一种分布式ID生成算法,其生成的 ID 具有全局唯一性。这种算法的核心思想是将 64 位的 Long 型 ID 分为四个部分:时间戳、工作机器ID、数据中心ID和序列号。

在上面我们进行数据插入的时候,由于未对主键生成策略进行配置,而 MP 默认采用的使用 Twitter 的 雪花算法。所以才会看到生成了一长串数字作为 ID。

插入一条数据

利用数据库的 ID 自增长

1、实体类中表示组件的属性上添加注解:@TableId(type = IdType.AUTO)

2、 数据库字段一定设置为自增😀

设置数据库字段为自增

3、再次运行插入测试:

ID自增长插入数据

手动生成 ID

1、实体类中表示组件的属性上添加注解:@TableId(type = IdType.INPUT)

2、修改测试类,手动设置 ID

1
2
3
4
5
6
7
8
9
10
@Test
void testSave() {
User user = new User();
user.setId(6L); // 手动设置 ID
user.setName("木又枯了");
user.setAge(18);
user.setEmail("example@gmail.com");
int i = userMapper.insert(user);
System.out.println(i);
}

PS:如果我们设置组件生成策略为手动输入 ,但是没有输入,这个时候日志就会显示插入的是 null,但是数据库中也会插入一条 ID 自增的记录

3、再次运行插入测试:

手动生成ID插入数据

4.3 @TableField

  • 描述:字段注解(非主键)
1
2
3
4
5
6
7
8
9
@TableName("sys_user")
public class User {
@TableId
private Long id;
@TableField("nickname")
private String name;
private Integer age;
private String email;
}

一般情况下我们并不需要给字段添加 @TableField 注解,一些特殊情况除外:

  • 成员变量名与数据库字段名不一致
  • 成员变量是以 isXXX 命名,按照 JavaBean 的规范,MP 识别字段时会把 is 去除,这就导致与数据库不符。
  • 成员变量名与数据库一致,但是与数据库的关键字冲突。使用 @TableField 注解给字段名添加转义字符:``

更多支持的其它属性如下请参考官方文档:@Tablefield注解

5. 常见配置

MP 也支持基于 yml 文件的自定义配置,详见官方文档:使用配置

大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:

  • 实体类的别名扫描包
  • 全局 ID 类型
1
2
3
4
5
mybatis-plus:
type-aliases-package: com.muyoukule.entity
global-config:
db-config:
id-type: auto # 全局id类型为自增长

需要注意的是,MP 也支持手写 SQL 的,而 mapper 文件的读取地址可以自己配置:

1
2
mybatis-plus:
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。

可以看到默认值是 classpath*:/mapper/**/*.xml,也就是说我们只要把 mapper.xml 文件放置这个目录下就一定会被加载。

6. 条件构造器

除了新增以外,修改、删除、查询的SQL语句都需要指定 where 条件。因此 BaseMapper 中提供的相关方法除了以 id 作为 where 条件以外,还支持更加复杂的 where 条件。

BaseMapper中的条件构造器

参数中的 Wrapper 就是条件构造的抽象类,其下有很多默认实现,继承关系如图:

Wrapper继承关系图

Wrapper 的子类 AbstractWrapper 提供了 where 中包含的所有条件构造方法:

AbstractWrapper中的方法

QueryWrapperAbstractWrapper 的基础上拓展了一个 select 方法,允许指定查询字段:

QueryWrapper的select方法

UpdateWrapperAbstractWrapper 的基础上拓展了一个 set 方法,允许指定 SQL 中的 SET 部分:

UpdateWrapper的set方法

接下来,我们就来看看如何利用 Wrapper 实现复杂查询。

6.1 QueryWrapper

当前数据表数据:

条件查询前数据库数据

无论是修改、删除、查询,都可以使用 QueryWrapper 来构建查询条件。接下来看一些例子:

多条件构建

查询出名字中带 o ,年龄大于等于 18 的人

1
2
3
4
5
6
7
8
9
10
11
@Test
void testQueryWrapper() {
// 1.构建查询条件 where name like "%o%" AND age >= 18
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "name", "age", "email")
.like("name", "o")
.ge("age", 18);
// 2.查询数据
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}

可以看到 MP 在编写 SQL 语句时会使用 ? 占位符,然后将参数传进去,最终查询到结果:

名字中带o年龄大于等于18

查询出年龄小于 20 或年龄大于 25 的人

1
2
3
4
5
6
7
8
9
@Test
void testQueryWrapper() {
// 1.构建查询条件 where (age < 25 OR age > 20)
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.lt("age", 20).or().gt("age", 25);
// 2.查询数据
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}

年龄小于20或年龄大于25

PS:or() 就相当于我们sql语句中的 or 关键字,不加默认是 and

查询投影

查询指定字段

1
2
3
4
5
6
7
@Test
void testQueryWrapper() {
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.select("name","age");
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}

查询name,age字段

聚合查询

count、max、min、avg、sum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
void testQueryWrapper() {
QueryWrapper<User> wrapper = new QueryWrapper<User>();
// SELECT count(*) as count FROM user
//wrapper.select("count(*) as count");
// SELECT max(age) as maxAge FROM user
//wrapper.select("max(age) as maxAge");
// SELECT min(age) as minAge FROM user
//wrapper.select("min(age) as minAge");
// SELECT sum(age) as sumAge FROM user
//wrapper.select("sum(age) as sumAge");
//SELECT avg(age) as avgAge FROM user
wrapper.select("avg(age) as avgAge");
List<Map<String, Object>> userList = userMapper.selectMaps(wrapper);
userList.forEach(System.out::println);
}

分组查询

分组查询,完成 group by 的查询使用

1
2
3
4
5
6
7
8
9
@Test
void testQueryWrapper() {
// SELECT count(*) as count,age FROM `user` GROUP BY age
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.select("count(*) as count,age");
wrapper.groupBy("age");
List<Map<String, Object>> list = userMapper.selectMaps(wrapper);
list.forEach(System.out::println);
}

分组查询age

根据 ID 降序排列

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void testQueryWrapper() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
/**
* condition :条件,返回boolean,
当condition为true,进行排序,如果为false,则不排序
* isAsc:是否为升序,true为升序,false为降序
* columns:需要操作的列
*/
wrapper.orderBy(true, false, "id");
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}

根据ID降序排列

更新用户名为 Jack 的用户的年龄为 18

1
2
3
4
5
6
7
8
9
@Test
void testUpdateByQueryWrapper() {
// 1.构建查询条件 where name = "Jack"
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("name", "Jack");
// 2.更新数据,user中非null字段都会作为set语句
User user = new User();
user.setAge(18);
userMapper.update(user, wrapper);
}

更新用户名为Jack的用户的年龄为18

除了上面介绍的这几种查询条件构建方法以外还会有很多其他的方法,比如 isNull,isNotNull,in,notIn 等等方法可供选择,具体参考官方文档的条件构造器来学习使用。

6.2 UpdateWrapper

基于 BaseMapper 中的 update 方法更新时只能直接赋值,对于一些复杂的需求就难以实现。

例如:更新 ID 为 1、2、4 的用户的年龄,减两岁,对应的 SQL 应该是:

1
UPDATE user SET age = age - 2 WHERE id in (1, 2, 4)

SET 的赋值结果是基于字段现有值的,这个时候就要利用 UpdateWrapper 中的 setSql 功能了:

1
2
3
4
5
6
7
8
9
10
@Test
void testUpdateWrapper() {
List<Long> ids = List.of(1L, 2L, 4L);
// 1.生成SQL
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("age = age - 2") // SET age = age - 2
.in("id", ids); // WHERE id in (1, 2, 4)
// 2.更新,注意第一个参数可以给 null,也就是不填更新字段和数据,而是基于 UpdateWrapper 中的 setSQL 来更新
userMapper.update(null, wrapper);
}

更新ID为1,2,4的用户的年龄减两岁

6.3 LambdaQueryWrapper

无论是 QueryWrapper 还是 UpdateWrapper 在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?

其中一种办法是基于变量的 gettter 方法结合反射技术。因此我们只要将条件对应的字段的 getter 方法传递给 MP,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的 方法引用Lambda 表达式。 因此 MP 又提供了一套基于 Lambda 的Wrapper,包含两个:

  • LambdaQueryWrapper
  • LambdaUpdateWrapper

分别对应 QueryWrapper 和 UpdateWrapper

其使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testLambdaQueryWrapper() {
// 1.构建条件 where name like "%o%" AND age >= 18
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.lambda()
.select(User::getId, User::getName, User::getAge, User::getEmail)
.like(User::getName, "o")
.ge(User::getAge, 18);
// 2.查询
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}

6.4 自定义SQL

在演示 UpdateWrapper 的案例中,我们在代码中编写了更新的 SQL 语句:

手写的SQL语句

这种写法在某些企业也是不允许的,因为 SQL 语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是 in 语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。 这实在是太麻烦了。假如查询条件更复杂,动态SQL的编写也会更加复杂。

所以,MP提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL。

6.4.1 基本用法

以当前案例来说,我们可以这样写:

1
2
3
4
5
6
7
8
@Test
void testCustomWrapper() {
// 1.准备自定义查询条件
List<Long> ids = List.of(1L, 2L, 4L);
QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);
// 2.调用mapper的自定义方法,直接传递Wrapper
userMapper.deductAgeByIds(2, wrapper);
}

然后在UserMapper中自定义SQL:

1
2
3
4
public interface UserMapper extends BaseMapper<User> {
@Select("UPDATE user SET age = age - #{age} ${ew.customSqlSegment}")
void deductAgeByIds(@Param("age") int age, @Param("ew") QueryWrapper<User> wrapper);
}

这样就省去了编写复杂查询条件的烦恼了。

6.4.2 多表关联

理论上来讲MyBatisPlus是不支持多表查询的,不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。

执行SQL脚本,创建address表,与 user 表相关联:

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
CREATE TABLE IF NOT EXISTS `address` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
`province` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '省',
`city` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '市',
`town` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '县/区',
`mobile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '手机',
`street` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '详细地址',
`contact` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '联系人',
`is_default` bit(1) DEFAULT b'0' COMMENT '是否是默认 1默认 0否',
`notes` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE,
KEY `user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=71 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=COMPACT;


INSERT INTO `address` (`id`, `user_id`, `province`, `city`, `town`, `mobile`, `street`, `contact`, `is_default`, `notes`) VALUES
(59, 2, '北京', '北京', '朝阳区', '13900112222', '金燕龙办公楼', 'Rose', b'1', NULL),
(60, 1, '北京', '北京', '朝阳区', '13700221122', '修正大厦', 'Jack', b'0', NULL),
(61, 1, '上海', '上海', '浦东新区', '13301212233', '航头镇航头路', 'Jack', b'1', NULL),
(63, 2, '广东', '佛山', '永春', '13301212233', '永春武馆', 'Rose', b'0', NULL),
(64, 3, '浙江', '杭州', '拱墅区', '13567809102', '浙江大学', 'Hope', b'1', NULL),
(65, 3, '浙江', '杭州', '拱墅区', '13967589201', '左岸花园', 'Hope', b'0', NULL),
(66, 4, '湖北', '武汉', '汉口', '13967519202', '天天花园', 'Thomas', b'1', NULL),
(67, 3, '浙江', '杭州', '拱墅区', '13967589201', '左岸花园', 'Hopey', b'0', NULL),
(68, 4, '湖北', '武汉', '汉口', '13967519202', '天天花园', 'Thomas', b'1', NULL),
(69, 3, '浙江', '杭州', '拱墅区', '13967589201', '左岸花园', 'Hopey', b'0', NULL),
(70, 4, '湖北', '武汉', '汉口', '13967519202', '天天花园', 'Thomas', b'1', NULL);

我们要查询出所有收货地址在北京的并且用户 ID 在 1、2、4 之中的用户 要是自己基于 Mybatis 实现 SQL,大概是这样的:

1
2
3
4
5
6
7
8
9
10
<select id="queryUserByIdAndAddr" resultType="com.muyoukule.entity.User">
SELECT *
FROM user u
INNER JOIN address a ON u.id = a.user_id
WHERE u.id
<foreach collection="ids" separator="," item="id" open="IN (" close=")">
#{id}
</foreach>
AND a.city = #{city}
</select>

可以看出其中最复杂的就是 WHERE 条件的编写,如果业务复杂一些,这里的 SQL 会更变态。

但是基于自定义 SQL 结合 Wrapper 的玩法,我们就可以利用 Wrapper 来构建查询条件,然后手写 SELECT 及 FROM 部分,实现多表查询。

查询条件这样来构建:

1
2
3
4
5
6
7
8
9
10
11
@Test
void testCustomJoinWrapper() {
// 1.准备自定义查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.in("u.id", List.of(1L, 2L, 4L))
.eq("a.city", "北京");

// 2.调用mapper的自定义方法
List<User> userList = userMapper.queryUserByWrapper(wrapper);
userList.forEach(System.out::println);
}

然后在UserMapper中自定义方法:

1
2
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew") QueryWrapper<User> wrapper);

当然,也可以在UserMapper.xml中写SQL:

1
2
3
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
</select>

7. 代码生成

在使用MP 以后,基础的MapperServicePO代码相对固定,重复编写也比较麻烦。因此MP 官方提供了代码生成器。

适用版本:mybatis-plus-generator 3.5.1 及其以上版本,对历史版本不兼容!

3.5.1 以下的请参考:代码生成器(旧)

首先需要添加依赖,MP 从 3.0.3 之后移除了代码生成器与模板引擎的默认依赖,需要手动添加相关依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3.1</version>
</dependency>
<!--模板引擎 Velocity(默认)-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>

创建代码生成类:

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
package com.muyoukule;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.fill.Column;
import com.baomidou.mybatisplus.generator.fill.Property;

import java.sql.Types;
import java.util.Collections;

/**
* @author 木又枯了
* @date 2024/4/4
*/
// 代码自动生成器
public class CodeGenerator {
public static void main(String[] args) {
// 代码生成器对象
// 数据库信息
FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/mybatis_plus", "root", "root")
// 1.全局配置
.globalConfig(builder -> {
builder.disableOpenDir()
.author("木又枯了") // 设置作者
.outputDir(System.getProperty("user.dir") + "/src/main/java") // 代码输出目录
//.enableSwagger() // 开启 swagger 模式
.dateType(DateType.TIME_PACK) // 时间策略
.commentDate("yyyy-MM-dd") // 注释日期
.build();
})
// 2.数据库配置
.dataSourceConfig(builder ->
builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
int typeCode = metaInfo.getJdbcType().TYPE_CODE;
if (typeCode == Types.SMALLINT) {
// 自定义类型转换
return DbColumnType.INTEGER;
}
return typeRegistry.getColumnType(metaInfo);
})
)
// 3.包配置
.packageConfig(builder -> {
builder.parent("com.muyoukule") // 设置父包名
.entity("entity") // Entity 包名
.service("service") // Service 包名
.serviceImpl("service.impl") //Service Impl 包名
.mapper("mapper") //Mapper 包名
.xml("AddressMapper.xml") //Mapper XML 包名
.controller("controller") // Controller 包名
.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper"))// 设置mapperXml生成路径
.build();
})
// 4.策略配置
.strategyConfig(builder -> {
builder
// 实体策略配置
.entityBuilder()
.enableLombok() // 开启 lombok 模型
.versionColumnName("version") // 乐观锁字段名(数据库字段)
.logicDeleteColumnName("deleted") // 逻辑删除字段名(数据库字段)
.addTableFills(new Column("create_time", FieldFill.INSERT)) // 添加表字段填充
.addTableFills(new Property("updateTime", FieldFill.INSERT_UPDATE)) // 添加表字段填充
.idType(IdType.AUTO) // 全局主键类型
.formatFileName("%s") // 格式化文件名称, %s为占位符,指代模块名称
// Controller 策略配置
.controllerBuilder()
.enableRestStyle() // 开启生成 @RestController 控制器
.formatFileName("%sController") // 格式化文件名称, %s为占位符,指代模块名称
.build();
})
// 5.执行生成操作
.execute();
}
}

以上代码按要求修改即可使用。对于代码生成器中的更多代码内容,我们可以直接从官方文档中获取代码进行修改。

8. 逻辑删除

对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:

  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为true
  • 查询时过滤掉标记为true的数据

一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。

为了解决这个问题,MP 就添加了对逻辑删除的支持。

PS:只有 MP 生成的SQL语句才支持自动的逻辑删除,自定义 SQL 需要自己手动处理逻辑删除。

1、我们给 user 表添加一个逻辑删除字段,设置默认值为 0 :

1
alter table user add deleted bit default b'0' null comment '逻辑删除';

添加逻辑删除字段deleted

2、给 user 实体添加 deleted 字段:

1
2
//逻辑删除
private Boolean deleted;

3、在application.yml中配置逻辑删除字段:

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

4、测试

首先,我们执行一个删除操作:

1
2
3
4
@Test
void testDelete() {
int i = userMapper.deleteById(1L);
}

方法与普通删除一模一样,但是底层的SQL逻辑变了:

底层的SQL逻辑改变

查看数据库,发现 ID 为1 的记录还在数据库,只是 deleted 字段变为 1 :

查询一下试试:

1
2
3
4
5
@Test
void testSelectAll() {
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}

会发现 ID为 1 的记录确实没有查询出来,而且 SQL 中也对逻辑删除字段做了判断:

SQL对逻辑删除字段做出判断

综上, 开启了逻辑删除功能以后,我们就可以像普通删除一样做CRUD,基本不用考虑代码逻辑问题。还是非常方便的。

注意: 逻辑删除本身也有自己的问题,比如:

  • 会导致数据库表垃圾数据越来越多,从而影响查询效率
  • SQL 中全都需要对逻辑删除字段做判断,影响查询效率

因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

9. 通用枚举

9.1 声明通用枚举属性

1、我们给 user 表添加一个 status 字段:

1
alter table user add `status` INT(10) NULL DEFAULT '1' COMMENT '使用状态(1正常 2冻结)';

2、定义一个用户状态的枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.muyoukule.enums;

import lombok.Getter;

@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结");
private final int value;
private final String desc;

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

3、给 user 实体添加 status 字段:

1
2
//使用状态(1正常 2冻结)
private UserStatus status;

要让 MP 处理枚举与数据库类型自动转换,我们必须告诉 MP,枚举中的哪个字段的值作为数据库值。

MP 提供了 @EnumValue 注解来标记枚举属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结");

@EnumValue // 标记数据库存的值是 value
private final int value;
private final String desc;

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

9.2 配置扫描通用枚举

PS:从 3.5.2 开始无需配置!😁

方式一:仅配置指定包内的枚举类使用 MybatisEnumTypeHandler

1
2
3
4
mybatis-plus:
# 设置枚举包扫描。3.5.2 版本开始,省略此配置。
# 支持统配符 * 或者 ; 分割
typeEnumsPackage: com.muyoukule.enums

当添加这个配置后,mybatis-plus 提供的 MybatisSqlSessionFactoryBean 会自动扫描包内合法的枚举类(使用了 @EnumValue 注解,或者实现了 IEnum 接口),分别为这些类注册使用 MybatisEnumTypeHandler

换句话说,只有指定包下的枚举类会使用新的 TypeHandler。其他包下,或者包内没有做相关改造的枚举类,仍然会使用 Mybatis 的 DefaultEnumTypeHandler。

方式二:直接指定 DefaultEnumTypeHandler

此方式用来 全局 修改 Mybatis 使用的 EnumTypeHandler。

在application.yaml文件中添加配置:

1
2
3
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

自定义配置类 MybatisPlusAutoConfiguration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MybatisPlusAutoConfiguration {

@Bean
public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
return properties -> {
GlobalConfig globalConfig = properties.getGlobalConfig();
globalConfig.setBanner(false);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
properties.setConfiguration(configuration);
};
}
}

查询一条数据查看:

1
2
3
4
5
6
7
8
{
"id": 2,
"name": "Jack",
"age": 16,
"email": "test2@baomidou.com",
"deleted": false,
"status": "NORMAL"
}

如何序列化枚举值为前端返回值?

在UserStatus枚举中通过 @JsonValue 注解标记 JSON 序列化时展示的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结");

@EnumValue // 标记数据库存的值是 value
private final int value;
@JsonValue //标记响应json值
private final String desc;

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

再次查看:

1
2
3
4
5
6
7
8
{
"id": 2,
"name": "Jack",
"age": 16,
"email": "test2@baomidou.com",
"deleted": false,
"status": "正常"
}

可以看到在 desc 上加了 @JsonValue 注解注解后 status 由 NORMAL 变为了 正常

10. JSON类型处理器

1、我们给 user 表添加一个 info 字段,是 JSON 类型:

1
alter table user add `info` JSON NOT NULL COMMENT '详细信息';

2、向 info 字段擦插入数据格式像这样:

1
{"intro": "佛系青年", "gender": "male"}

3、给 user 实体添加 info 字段:

1
2
// 详细信息
private String info;

一般 User 实体类中都是 String 类型的 info,这样一来,我们要读取 info 中的属性时就非常不方便。

1
2
3
4
5
6
7
8
9
{
"id": 2,
"name": "Jack",
"age": 16,
"email": "test2@baomidou.com",
"deleted": false,
"status": "正常",
"info": "{\"intro\": \"佛系青年\", \"gender\": \"male\"}"
}

如果要方便获取,info 的类型最好是一个 Map 或者实体类。而一旦我们把 info 改为 对象 类型,就需要在写入数据库时手动转为 String,再读取数据库时,手动转换为 对象,这会非常麻烦。

因此 MP 提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理 JSON 就可以使用 JacksonTypeHandler 处理器。

怎么使用 JacksonTypeHandler 处理器呢?

1、我们定义一个单独实体类来与 info 字段的属性匹配:

1
2
3
4
5
@Data
public class UserInfo {
private String intro;
private String gender;
}

2、接下来,将 User 类的 info 字段设置为 UserInfo 类型,并声明类型处理器:

PS:必须开启映射注解 @TableName(autoResultMap = true) !!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "`user`", autoResultMap = true)
public class User {
@TableId(type = IdType.INPUT)
private Long id;

// -- skip --

// 详细信息
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
}

3、测试可以发现,所有数据都正确封装到 UserInfo 当中了:

1
2
3
4
5
6
7
8
9
10
11
12
{
"id": 2,
"name": "Jack",
"age": 16,
"email": "test2@baomidou.com",
"deleted": false,
"status": "正常",
"info": {
"intro": "伏地魔",
"gender": "male"
}
}

11. 自动填充功能

创建时间、更新时间,对于这两个字段的操作我们希望是自动完成而不是需要手动编写。

1、我们给 user 表添加 create_time 字段和 update_time 字段并将这两个字段类型设置为 timestamp

1
2
alter table user add `create_time` timestamp COMMENT '创建时间';
alter table user add `update_time` timestamp COMMENT '更新时间';

2、同步实体类,实体类的属性上增加注解:

1
2
3
4
5
// 字段填充内容
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

3、编写处理器来处理这些注解:

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());
}
}

3、测试插入:

1
2
3
4
5
6
7
8
9
@Test
void testSave() {
User user = new User();
user.setName("木又枯了");
user.setAge(18);
user.setEmail("example@gmail.com");
// 这里使用 MP 默认主键 ID 生成策略
userMapper.insert(user);
}

4、查看数据库:

数据自动填充

12. 乐观锁插件

与乐观锁相对的有:悲观锁

  • 悲观锁在数据修改前加锁,避免其他事务修改,确保数据安全但可能降低并发性能。
  • 乐观锁则假设冲突少,只在数据提交时验证冲突,提高并发性能但可能需处理更多冲突。

官方解释:乐观锁插件

目的:当要更新一条记录的时候,希望这条记录没有被别人更新。

乐观锁实现方式:

  • 取出记录时,获取当前 version
  • 更新时,带上这个 version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果 version 不对,就更新失败
1
2
3
4
-- 假设有两个线程 A、B 同时修改一条记录
update user set name = "木又枯了", version = version + 1 where id = 6 and version = 1
-- 但是 B 抢先完成了修改,version 修改成 2
-- 这个时候 A 就不能修改数据了,实现线程通信的安全

乐观锁插件实现步骤

1、我们给 user 表添加一个 version 字段,设置默认值为 1 :

1
alter table user add version int default b'1' null comment '乐观锁';

添加结果如下:

添加version字段

2、给实体类增加相应的字段,并添加注解 @Version

1
2
@Version
private Integer version;

3、注册组件

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@MapperScan("com.muyoukule.mapper")
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mybatisPlusInterceptor;
}
}

4、编写测试方法进行测试:

PS:要想实现乐观锁,首先第一步应该是拿到表中的 version,然后拿 version 当条件再将 version 加 1 更新回到数据库表中,所以我们需要先对其进行查询

1
2
3
4
5
6
7
8
9
10
11
12
// 测试乐观锁
@Test
public void testOptimisticLocker() {
// 查询
User user = userMapper.selectById(2L);
// 修改信息
user.setName("muyoukule");
user.setAge(18);
user.setEmail("example@gmail.com");
// 更新信息
userMapper.updateById(user);
}

5、查看数据库:

乐观锁修改成功

我们再来测试一下修改失败的情况,模拟一种加锁的情况,看看能不能实现多个人修改同一个数据的时候,只能有一个人修改成功。

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testOptimisticLocker2() {
// 1.先通过要修改的数据 id 将当前数据查询出来
User user = userMapper.selectById(2L); //version=2
User user2 = userMapper.selectById(2L); //version=2
user2.setName("muyoukule222");
userMapper.updateById(user2); //version=>3

user.setName("muyoukule111");
userMapper.updateById(user); //verion=2?条件还成立吗?
}

如果没有乐观锁,最后一次的修改会覆盖前一次的修改,但是有了乐观锁就不会出现这种情况了。查看数据库修改结果:

乐观锁修改失败

查看数据库后发现 muyoukule111 并未覆盖 muyoukule222

12. 分页插件

在未引入分页插件的情况下,MP 是不支持分页功能的,IServiceBaseMapper 中的分页方法都无法正常起效。 所以,我们必须配置分页插件。

1、配置分页插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@MapperScan("com.muyoukule.mapper")
public class MybatisPlusConfig {

/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加
//interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType
return interceptor;
}
}

2、编写一个分页查询的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void testPageQuery() {
// 1.分页查询,new Page()的两个参数分别是:页码、每页大小
Page<User> page = new Page<>(2, 2);
userMapper.selectPage(page, null);
// 2.总条数
System.out.println("total = " + page.getTotal());
// 3.总页数
System.out.println("pages = " + page.getPages());
// 4.数据
List<User> records = page.getRecords();
records.forEach(System.out::println);
}

3、控制台输出:

1
2
3
4
total = 8
pages = 4
User(id=4, name=Sandy, age=19, email=test4@baomidou.com, deleted=false, status=NORMAL, info=null, createTime=null, updateTime=null, version=1)
User(id=5, name=Billie, age=24, email=test5@baomidou.com, deleted=false, status=NORMAL, info=null, createTime=null, updateTime=null, version=1)

PS:由于前面我们使用了逻辑删除,deleted 值为 1 的字段不会被查询到,所以数据表一共是 8 条记录。

4、对比数据库数据:

分页查询数据库数据对比