0%

Mybatis-Plus

大家在日常开发中应该能发现,单表的CRUD功能代码重复度很高,也没有什么难度。而这部分代码量往往比较大,开发起来比较费时。

因此,目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是MybatisPlus.

官方网站: MyBatis-Plus

快速入门

HelloWorld

首先我们通过一个入门案例来感受MybatisPlus的魅力

  1. 准备数据库表和表对应的实体类
1
2
3
4
5
6
7
8
9
10
11
create database mp;
create table user
(
id bigint auto_increment
primary key,
name varchar(32) not null,
password varchar(32) not null,
age int not null,
tel varchar(32) not null,
);
insert into
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
  1. application.yaml中修改jdbc参数为你自己的数据库参数
1
2
3
4
5
6
7
8
9
10
11
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
logging:
level:
com.example: debug
pattern:
dateformat: HH:mm:ss
  1. 引入MybatisPlus的依赖

MybatisPlus提供了starter,实现了自动Mybatis以及MybatisPlus的自动装配功能,坐标如下:

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.4</version>
</dependency>

由于这个starter包含对mybatis的自动装配,因此完全可以替换掉Mybatis的starter。 最终,项目的依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
  1. 定义Mapper

为了简化单表CRUD,MybatisPlus提供了一个基础的BaseMapper接口,其中已经实现了单表的CRUD:

d7225118-532e-487f-a1d5-0c39625563b1

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

1
public interface UserMapper extends BaseMapper<User> {}
  1. 测试
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
@SpringBootTest
public class Test1 {
@Autowired
private UserMapper userMapper;

/**
* 查询全部
*/
@Test
public void testSelectList() {
QueryWrapper<User> wrapper = new QueryWrapper<User>();
//wrapper.lt("age", 18);

// lambda表达式
wrapper.lambda().lt(User::getAge, 10);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

/**
* 插入数据
*/
@Test
public void testInsert() {
User user = new User();
user.setAge(18);
user.setName("李四");
user.setStatus(UserStatus.NORMAL);
user.setInfo(UserInfo.builder()
.gender("男")
.hobby("学习")
.salary(5000).build());
user.setPassword("123456");
user.setTel("123546");

userMapper.insert(user);
}

/**
* 修改数据
*/
@Test
public void testUpdate() {
userMapper.updateById(User.builder()
.id(8L)
.age(10)
.status(UserStatus.NORMAL)
.build());
}
@Test
public void testUpdate1() {
User user1 = userMapper.selectById(8L);
User user2 = userMapper.selectById(8L);
user1.setName("张二");
user2.setName("张四");
userMapper.updateById(user1);
userMapper.updateById(user2);

}
/**
* 根据id查询信息
*/
@Test
public void testGetById() {
userMapper.selectById(8L);
}

/**
* 删除数据,根据id
*/
@Test
public void testDelete() {
userMapper.deleteById(9L);
}
}

常见注解

在刚刚的入门案例中,我们仅仅引入了依赖,继承了BaseMapper就能使用MybatisPlus,非常简单。但是问题来了: MybatisPlus如何知道我们要查询的是哪张表?表中有哪些字段呢?

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

泛型中的User就是与数据库对应的实体类.

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

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

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

@TableName

  • 描述:表名注解,标识实体类对应的数据库表
  • 使用位置:实体类
  • 示例:
1
2
3
4
5
@TableName("user")
public class User {
private Long id;
private String name;
}

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

@TableId

  • 描述:主键注解,标识实体类中的主键字段
  • 使用位置:实体类的主键字段
  • 示例:
1
2
3
4
5
6
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
}

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)

这里比较常见的有三种:

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

@TableField

  • 描述:普通字段注解
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String name;
@TableField(select = false)
private String password;
private Integer age;
private String tel;
}

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

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

支持的其它属性如下:

image-20231028085509709

image-20231028085524792

常见配置

MybatisPlus也支持基于yaml文件的自定义配置,详见官方文档:

https://www.baomidou.com/pages/56bac0/#%E5%9F%BA%E6%9C%AC%E9%85%8D%E7%BD%AE

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

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

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

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

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

例如,我们新建一个UserMapper.xml文件:

fa43db10-3203-4e60-9dcd-c07e8512532e

然后再其中定义一个方法:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<select id="queryById" resultType="com.example.popj.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>

然后在测试类UserMapperTest中测试该方法:

1
2
3
4
5
@Test
void testQuery() {
User user = userMapper.queryById(1L);
System.out.println("user = " + user);
}

核心功能

刚才讲的都是以id为条件的简单CRUD,一些复杂的SQL语句就需要用到更高级的功能了

条件构造器

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

030881d3-cd35-48cc-a68e-981e72a0c770

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

c8a32e0d-bc8c-4dda-b511-94501eac2a73

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

7f9fb1f7-563e-4894-9684-cd787151cd20

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

20651c2a-a88b-40e8-8ca8-dbba9c0f7bfb

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

img

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

QueryWrapper

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

查询:查询出名字中带o的,年龄大于等于15岁的人。代码如下:

1
2
3
4
5
6
7
8
9
10
11
@Test
void testQueryWrapper() {
// 1.构建查询条件 where name like "%o%" AND age >= 15
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "name", "age", "tel") // 查询那些字段
.like("name", "o") // like
.ge("age", 15); // ge代表大于等于
// 2.查询数据
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

更新:更新用户名为jack的用户的年龄为20,代码如下:

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.setBalance(2000);
userMapper.update(user, wrapper);
}

UpdateWrapper

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

例如:更新id为1,2,4的用户的年龄,扣2,对应的SQL应该是:

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

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

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

LambdaQueryWrapper(推荐)

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

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

  • LambdaQueryWrapper
  • LambdaUpdateWrapper

分别对应QueryWrapper和UpdateWrapper

其使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
@Test
void testLambdaQueryWrapper() {
// 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getName, "o")
.ge(User::getAge, 15);
// 2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

自定义SQL

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

其中有setSql("age = age - 2")

这种写法在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。

这实在是太麻烦了。假如查询条件更复杂,动态SQL的编写也会更加复杂。

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

基本用法

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

1
2
3
4
5
6
7
8
9
@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.deductBalanceByIds(200, wrapper);
}

然后在UserMapper中自定义SQL:

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

这里必须写成”ew”,如果你忘了可以写Constants.WRAPPER

多表关联

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

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

1
2
3
4
5
6
7
8
9
10
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.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
12
@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> users = userMapper.queryUserByWrapper(wrapper);

users.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>

Service接口

MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。 通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以分为以下几类:

  • save:新增
  • remove:删除
  • update:更新
  • get:查询单个结果
  • list:查询集合结果
  • count:计数
  • page:分页查询

CRUD

我们先俩看下基本的CRUD接口。 新增

img

  • save是新增单个元素
  • saveBatch是批量新增
  • saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增
  • saveOrUpdateBatch是批量的新增或修改

删除:

img

  • removeById:根据id删除
  • removeByIds:根据id批量删除
  • removeByMap:根据Map中的键值对为条件删除
  • remove(Wrapper<T>):根据Wrapper条件删除
  • ~~removeBatchByIds~~:暂不支持

修改:

img

  • updateById:根据id修改
  • update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含setwhere部分
  • update(T,Wrapper<T>):按照T内的数据修改与Wrapper匹配到的数据
  • updateBatchById:根据id批量修改

Get:

img

  • getById:根据id查询1条数据
  • getOne(Wrapper<T>):根据Wrapper查询1条数据
  • getBaseMapper:获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper

List:

img

  • listByIds:根据id批量查询
  • list(Wrapper<T>):根据Wrapper条件查询多条数据
  • list():查询所有

Count

img

  • count():统计所有数量
  • count(Wrapper<T>):统计符合Wrapper条件的数据数量

getBaseMapper: 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:

img

基本用法

由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承IService以拓展方法。同时,让自定义的Service实现类继承ServiceImpl,这样就不用自己实现IService中的接口了。

首先,定义IUserService,继承IService

1
2
3
4
5
6
7
8
package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface IUserService extends IService<User> {
// 拓展自定义方法
}

然后,编写UserServiceImpl类,继承ServiceImpl,实现UserService

1
2
3
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {}

项目结构如下:

img

接下来,我们快速实现下面4个接口:

编号 接口 请求方式 请求路径 请求参数 返回值
1 新增用户 POST /users 用户表单实体
2 删除用户 DELETE /users/{id} 用户id
3 根据id查询用户 GET /users/{id} 用户id 用户VO
4 根据id批量查询 GET /users 用户id集合 用户VO集合

微服务框架

-

空间目录

-

day01-MybatisPlus

-

day02-Docker

-

day03-微服务01

-

微服务拆分作业参考

-

day04-微服务02

-

day05-服务保护和分布式事务

-

day06-MQ基础

-

day07-MQ高级

-

day08-Elasticsearch

-

day09-Elasticsearch02

-

day10-Redis面试篇

-

day11-微服务面试篇

-

day01-MybatisPlus

最新修改时间为10月12日

登录/注册

day01-MybatisPlus

3596

8010

13

2

img

接下来,我们快速实现下面4个接口:

编号 接口 请求方式 请求路径 请求参数 返回值
1 新增用户 POST /users 用户表单实体
2 删除用户 DELETE /users/{id} 用户id
3 根据id查询用户 GET /users/{id} 用户id 用户VO
4 根据id批量查询 GET /users 用户id集合 用户VO集合

首先,我们在项目中引入几个依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>

然后需要配置swagger信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: email
concat: jiahao
url: jiahao.love
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.example.controller

然后,接口需要两个实体:

  • UserDTO:代表新增时的用户表单
  • UserVO:代表查询的返回结果

首先是UserDTO

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@ApiModel(description = "用户表单实体")
public class UserDTO {
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("年龄")
private Integer age;
@ApiModelProperty("电话")
private String tel;
}

然后是UserVO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("年龄")
private Integer age;
@ApiModelProperty("电话")
private String tel;
@ApiModelProperty("状态 (0 冻结 1 正常)")
private UserStatus status;
@ApiModelProperty("地址")
private String address;
}

最后,按照Restful风格编写Controller接口方法:

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
@Slf4j
@RestController
@Api(tags = "用户相关接口")
@RequestMapping("/users")
public class UserController {
@Autowired
private UserIService userIService;
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public Result<UserVO> getById(@PathVariable Long id) {
return Result.success(userIService.selectById(id));
}

@GetMapping
@ApiOperation("根据id批量查询")
public Result<List<UserVO>> getByIds(@RequestParam("ids") List<Long> ids) {
return Result.success(userIService.selectBathIds(ids));
}

@PostMapping
@ApiOperation("新增用户")
public Result save(@RequestBody UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO, user);
userIService.save(user);
return Result.success();
}

@DeleteMapping("/{id}")
@ApiOperation("删除用户")
public Result delete(@PathVariable Long id) {
// 由于指明了逻辑字段,所以删除时只会修改逻辑字段为delete
userIService.removeById(id);
return Result.success();
}
}

可以看到上述接口都直接在controller即可实现,无需编写任何service代码,非常方便。

不过,一些带有业务逻辑的接口则需要在service中自定义实现了。例如下面的需求:

  • 根据id扣减用户年龄

这看起来是个简单修改功能,只要修改用户余额即可。但这个业务包含一些业务逻辑处理:

  • 判断用户状态是否正常
  • 判断用户年龄是否充足

这些业务逻辑都要在service层来做,另外更新余额需要自定义SQL,要在mapper中来实现。因此,我们除了要编写controller以外,具体的业务还要在service和mapper中编写。

首先在UserController中定义一个方法:

1
2
3
4
5
6
@PutMapping("/{id}/deduction/{age}")
@ApiOperation("根据id减少年龄")
public Result deductBalance(@PathVariable("id")Long id , @PathVariable("age") Integer age) {
userIService.deductAge(id, age);
return Result.success();
}

然后是UserService接口:

1
2
3
4
5
6
/**
* 根据id减少年龄
* @param id
* @param age
*/
void deductAge(Long id, Integer age);

最后是UserServiceImpl实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 根据id减少年龄
* @param id
* @param age
*/
@Override
public void deductAge(Long id, Integer age) {
// 查询账户
User user = userMapper.selectById(id);
// 判断账户是否正常
if (user.getStatus().equals(0)) throw new RuntimeException("账户异常");
// 判断账户年龄
if (user.getAge() <= age) {
user.setStatus(UserStatus.FREEZE);
}

// 扣减年龄
lambdaUpdate().set(User::getAge, user.getAge() - age)
.set(User::getStatus, user.getStatus())
.eq(User::getId, id)
.eq(User::getAge, user.getAge()) // 乐观锁
.update();
}

Lambda

IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。我们通过两个案例来学习一下。

案例一:实现一个根据复杂条件查询用户的接口,查询条件如下:

  • name:用户名关键字,可以为空
  • status:用户状态,可以为空
  • minAge:最小年龄,可以为空
  • maxAge:最大年龄,可以为空

可以理解成一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断。

我们首先需要定义一个查询条件实体,UserQueryDTO实体:

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@ApiModel(description = "用户条件查询实体")
public class UserQueryDTO {
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("状态 (1 正常 0 冻结)")
private Integer status;
@ApiModelProperty("最小年龄")
private Integer minAge;
@ApiModelProperty("最大年龄")
private Integer maxAge;
}

接下来我们在UserController中定义一个controller方法:

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/queryList")
@ApiOperation("条件查询用户")
public Result<List<UserVO>> queryList(UserQueryDTO userQueryDTO) {
// 查询用户
List<User> list = userIService.lambdaQuery()
.like(userQueryDTO.getName() != null, User::getName, userQueryDTO.getName())
.eq(userQueryDTO.getStatus() != null, User::getStatus, userQueryDTO.getStatus())
.ge(userQueryDTO.getMinAge() != null, User::getAge, userQueryDTO.getMinAge())
.le(userQueryDTO.getMaxAge() != null, User::getAge, userQueryDTO.getMaxAge())
.list();
return Result.success(BeanUtil.copyToList(list, UserVO.class));
}

在组织查询条件的时候,我们加入了 name != null 这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的<if>标签。这样就实现了动态查询条件效果了。

可以发现lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个list(),这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用list(),可选的方法有:

  • .one():最多1个结果
  • .list():返回集合结果
  • .count():返回计数结果

MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。

与lambdaQuery方法类似,IService中的lambdaUpdate方法可以非常方便的实现复杂更新业务。

例如下面的需求:

需求:改造根据id修改用户余额的接口,要求如下

  • 如果扣减后年龄为0,则将用户status修改为冻结状态(2)

也就是说我们在扣减用户余额时,需要对用户剩余余额做出判断,如果发现剩余余额为0,则应该将status修改为2,这就是说update语句的set部分是动态的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void deductAge(Long id, Integer age) {
// 查询账户
User user = userMapper.selectById(id);
// 判断账户是否正常
if (user.getStatus().equals(0)) throw new RuntimeException("账户异常");
// 判断账户年龄
if (user.getAge() <= age) {
user.setStatus(UserStatus.FREEZE);
}

// 扣减年龄
lambdaUpdate().set(User::getAge, user.getAge() - age)
.set(User::getStatus, user.getStatus())
.eq(User::getId, id)
.eq(User::getAge, user.getAge()) // 乐观锁
.update();
}

这里的UserStatus为枚举类,FREEZE代表冻结

批量新增

IService中的批量新增功能使用起来非常方便,但有一点注意事项,我们先来测试一下。 首先我们测试逐条插入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
void testSaveOneByOne() {
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
userService.save(buildUser(i));
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}

private User buildUser(int i) {
User user = new User();
user.setUsername("user_" + i);
user.setPassword("123");
user.setPhone("" + (18688190000L + i));
user.setBalance(2000);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(user.getCreateTime());
return user;
}

执行结果如下:

img

可以看到速度非常慢。

然后再试试MybatisPlus的批处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
void testSaveBatch() {
// 准备10万条数据
List<User> list = new ArrayList<>(1000);
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
list.add(buildUser(i));
// 每1000条批量插入一次
if (i % 1000 == 0) {
userService.saveBatch(list);
list.clear();
}
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}

执行最终耗时如下:

img

可以看到使用了批处理以后,比逐条新增效率提高了10倍左右,性能还是不错的。

不过,我们简单查看一下MybatisPlus源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
// ...SqlHelper
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {
int size = list.size();
int idxLimit = Math.min(batchSize, size);
int i = 1;
for (E element : list) {
consumer.accept(sqlSession, element);
if (i == idxLimit) {
sqlSession.flushStatements();
idxLimit = Math.min(idxLimit + batchSize, size);
}
i++;
}
});
}

可以发现其实MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。SQL类似这样:

1
2
3
4
Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01

而如果想要得到最佳性能,最好是将多条SQL合并为一条,像这样:

1
2
3
4
5
6
INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )
VALUES
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);

该怎么做呢?

MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements。顾名思义,就是重写批处理的statement语句。参考文档:

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements

这个参数的默认值是false,我们需要修改连接参数,将其配置为true

修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true:

1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root

再次测试插入10万条数据,可以发现速度有非常明显的提升:

img

ClientPreparedStatementexecuteBatchInternal中,有判断rewriteBatchedStatements值是否为true并重写SQL的功能:

最终,SQL被重写了:

img

扩展功能

代码生成

在使用MybatisPlus以后,基础的MapperServicePO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成POMapperService等相关代码。只不过代码生成器同样要编码使用,也很麻烦。

这里推荐大家使用一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。

安装插件

Idea的plugins市场中搜索并安装MyBatisPlus插件:

img

然后重启你的Idea即可使用。

使用

刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到other,选择Config Database

img

在弹出的窗口中填写数据库连接的基本信息:

img

点击OK保存。

然后再次点击Idea顶部菜单中的other,然后选择Code Generator:

img

在弹出的表单中填写信息:

img

最终,代码自动生成到指定的位置了

静态工具

有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:

3.5.4之后有这个工具类Db

img

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
void testDbGet() {
User user = Db.getById(1L, User.class);
System.out.println(user);
}

@Test
void testDbList() {
// 利用Db实现复杂条件查询
List<User> list = Db.lambdaQuery(User.class)
.like(User::getName, "o")
.ge(User::getAge, 15)
.list();
list.forEach(System.out::println);
}

@Test
void testDbUpdate() {
Db.lambdaUpdate(User.class)
.set(User::getAge, 20)
.eq(User::getUsername, "Rose");
}

逻辑删除

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

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

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

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

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

例如,我们给address表添加一个逻辑删除字段:

1
alter table user add status int default 1 null comment '状态';

然后给User实体添加status字段:

1
2
@TableLogic
private UserStatus status;

接下来,我们要在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)

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

1
2
3
4
5
@Test
void testDeleteByLogic() {
// 删除方法与以前没有区别
addressService.removeById(59L);
}

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

img

查询一下试试:

1
2
3
4
5
@Test
void testQuery() {
List<Address> list = addressService.list();
list.forEach(System.out::println);
}

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

img

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

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

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

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

通用枚举

User类中有一个用户状态字段:

像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举Integer转换,非常麻烦。

因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换

定义枚举

我们定义一个用户状态的枚举:

1
2
3
4
5
6
7
8
9
10
11
@AllArgsConstructor
@Getter
public enum UserStatus {

NORMAL(1, "正常"),
FREEZE(0, "冻结");
@EnumValue // 告知mp哪个字段的值作为数据库值
private final Integer value;
@JsonValue // 标记json序列化时展示的字段
private final String desc;
}

然后把User类中的status字段改为UserStatus 类型:

要让MybatisPlus处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。 MybatisPlus提供了@EnumValue注解来标记枚举属性:

配置枚举处理器

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

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

测试

1
2
3
4
5
@Test
void testService() {
List<User> list = userService.list();
list.forEach(System.out::println);
}

最终,查询出的User类的status字段会是枚举类型:

img

同时,为了使页面查询结果也是枚举格式,我们需要修改UserVO中的status属性:

img

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

img

最后,在页面查询,结果如下:

img

JSON类型处理器

数据库的user表中有一个info字段,是JSON类型:

image-20231027210923322

而目前User实体类中却是String类型:

img

这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map或者实体类。

而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。

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

接下来,我们就来看看这个处理器该如何使用。

定义实体

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

1
2
3
4
5
6
7
8
9
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
private Integer salary;
private String hobby;
private String gender;
}

使用类型处理器

接下来,将User类的info字段修改为UserInfo类型,并声明类型处理器:

1
2
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;

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

插件功能

MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:

  • PaginationInnerInterceptor:自动分页
  • TenantLineInnerInterceptor:多租户
  • DynamicTableNameInnerInterceptor:动态表名
  • OptimisticLockerInnerInterceptor:乐观锁
  • IllegalSQLInnerInterceptor:sql 性能规范
  • BlockAttackInnerInterceptor:防止全表更新与删除

注意: 使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:

  • 多租户,动态表名
  • 分页,乐观锁
  • sql 性能规范,防止全表更新与删除

这里我们以分页插件为里来学习插件的用法。

配置分页插件

在项目中新建一个配置类:

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

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建MybatisPlusInterceptor拦截器对象
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 添加乐观锁拦截器
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}

分页API

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 分页查询
*/
@Test
void testSelectPage() {
// 创建分页对象,设置分页参数
Page<User> page = new Page<>(1, 3);
page.addOrder(new OrderItem("age", false));
// 执行分页查询
userMapper.selectPage(page, null);
//3 获取分页结果
System.out.println("当前页码值:"+page.getCurrent());
System.out.println("每页显示数:"+page.getSize());
System.out.println("总页数:"+page.getPages());
System.out.println("总条数:"+page.getTotal());
System.out.println("当前页数据:"+page.getRecords());
}

这里用到了分页参数,Page,即可以支持分页参数,也可以支持排序参数。常见的API如下:

1
2
3
4
5
6
7
int pageNo = 1, pageSize = 5;
// 分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("age", false));

userService.page(page);