MybatisPlus
此笔记基于黑马课程
快速入门
资料:https://wwbum.lanzoue.com/iHOeZ3dayvhe
环境准备
打开资料中的mp-demo项目,项目JDK版本为11
在数据库中导入mp.sql脚本
在application.yaml中按照自己的实际情况修改数据库参数
快速开始
导入MybatisPlus依赖
1
2
3
4
5
|
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
|
由于这个starter包含对mybatis的自动装配,因此完全可以替换掉之前Mybatis的starter
继承Mapper接口
MybatisPlus提供了一个基础的BaseMapper接口,其中已经实现了单表的CRUD:
所以在自己的Mapper接口上继承BaseMapper接口,并指定泛型为自己的实体类,这样BaseMapper才知道操作的是哪个实体
1
|
public interface UserMapper extends BaseMapper<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
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
|
package com.itheima.mp.mapper;
import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.List;
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testInsert() {
User user = new User();
user.setId(5L);
user.setUsername("Lucy");
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
// userMapper.saveUser(user); 原本的Mybatis方式
userMapper.insert(user);
}
@Test
void testSelectById() {
// User user = userMapper.queryUserById(5L);
User user = userMapper.selectById(5L);
System.out.println("user = " + user);
}
@Test
void testQueryByIds() {
// List<User> users = userMapper.queryUserByIds(List.of(1L, 2L, 3L, 4L));
List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L));
users.forEach(System.out::println);
}
@Test
void testUpdateById() {
User user = new User();
user.setId(5L);
user.setBalance(20000);
// userMapper.updateUser(user);
userMapper.updateById(user);
}
@Test
void testDeleteUser() {
// userMapper.deleteUser(5L);
userMapper.deleteById(5L);
}
}
|
发现都能正常使用,所以只需要继承BaseMapper就能省去所有的单表CRUD,并且提供的方法都是见名知意,无需特地去记
常见注解
MybatisPlus如何知道要查询的是哪张表?表中有哪些字段呢?
答案是通过继承时指定的泛型
刚刚继承时泛型中的User就是与数据库对应的PO
MybatisPlus就是根据PO实体的信息来推断出表的信息,从而生成SQL的。
默认情况下:
- MybatisPlus会把PO实体的类名驼峰转下划线作为表名
- MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
- MybatisPlus会把名为id的字段作为主键
但很多情况下,默认的实现与实际场景不符,因此MybatisPlus也提供了一些注解便于我们声明表信息。
@TableName
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
示例:如果在数据库中表名叫tb_user但实体类却叫User,就可以使用该注解手动标识
1
2
3
4
5
|
@TableName("tb_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[] |
否 |
{} |
需要排除的属性名 |
@TableId
- 描述:主键注解,标识实体类中的主键字段
- 使用位置:实体类的主键字段
1
2
3
4
5
6
7
|
public class User {
// 映射到数据库表的 "id" 列
@TableId(value = "id",type = IdType.AUTO)
private Long userId;
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
15
16
17
|
public class User {
private Long id;
private String name;
private Integer age;
// MybatisPlus识别字段时会把is去除
@TableField("is_married")
private Boolean isMarried;
// 与数据库的关键字冲突,添加转义字符``
@TableField("`concat`")
private String concat;
// 数据库中不存在该字段但实体类中却定义了该属性
@TableField(exist = false)
private String hahaha;
}
|
一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外:
- 成员变量名与数据库字段名不一致
- 成员变量是以
isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。
- 成员变量名与数据库一致,但是与数据库的关键字冲突。使用
@TableField注解给字段名添加转义字符:``
- 定义了数据库中不存在的成员变量
支持的其它属性如下:
| 属性 |
类型 |
必填 |
默认值 |
描述 |
| value |
String |
否 |
"" |
数据库字段名 |
| exist |
boolean |
否 |
true |
是否为数据库表字段 |
| condition |
String |
否 |
"" |
字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window) |
| update |
String |
否 |
"" |
字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
| insertStrategy |
Enum |
否 |
FieldStrategy.DEFAULT |
举例:NOT_NULL insert into table_a(column) values (#{columnProperty}) |
| updateStrategy |
Enum |
否 |
FieldStrategy.DEFAULT |
举例:IGNORED update table_a set column=#{columnProperty} |
| whereStrategy |
Enum |
否 |
FieldStrategy.DEFAULT |
举例:NOT_EMPTY where column=#{columnProperty} |
| fill |
Enum |
否 |
FieldFill.DEFAULT |
字段自动填充策略 |
| select |
boolean |
否 |
true |
是否进行 select 查询 |
| keepGlobalFormat |
boolean |
否 |
false |
是否保持使用全局的 format 进行处理 |
| jdbcType |
JdbcType |
否 |
JdbcType.UNDEFINED |
JDBC 类型 (该默认值不代表会按照该值生效) |
| typeHandler |
TypeHander |
否 |
|
类型处理器 (该默认值不代表会按照该值生效) |
| numericScale |
String |
否 |
"" |
指定小数点后保留的位数 |
常见配置
1
2
3
4
5
6
7
8
9
10
|
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po # 别名扫描包,在配置后可以直接在xml文件中写类名,不用写全类名了
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值
configuration:
map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射,当前这个是默认值
cache-enabled: false # 是否开启二级缓存,当前这个是默认值
global-config:
db-config:
id-type: auto # 全局id类型为自增长
update-strategy: not_null # 更新策略:只更新非空字段
|
更详细的配置说明可以查询官网使用配置 | MyBatis-Plus或者AI
核心功能
条件构造器
BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件

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

Wrapper的子类AbstractWrapper提供了where可以使用的SQL语法中的所有条件的构造方法
而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段
而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分
QueryWrapper
无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件
例题:
查询:查询出名字中带o的,存款大于等于1000元的人
SQL语句:
1
2
3
|
SELECT id,username,info,balance
FROM user
WHERE username LIKE "%o%" AND balance >= 1000
|
对应代码:
1
2
3
4
5
6
7
8
9
10
11
12
|
@Test
void testQueryWrapper() {
// 1.构建查询条件 where name like "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
// 2.查询数据
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
|
更新:更新用户名为jack的用户的余额为2000
SQL语句:
1
2
3
|
UPDATE user
SET balance = 2000
WHERE (username = "jack")
|
对应代码:
1
2
3
4
5
6
7
8
9
10
|
@Test
void testUpdateByQueryWrapper() {
// 1.构建查询条件 where name = "Jack"
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");
// 2.更新数据,user中非null字段都会作为set语句
User user = new User();
user.setBalance(2000);
userMapper.update(user, wrapper);
}
|
UpdateWrapper
基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现
例如:更新id为1,2,4的用户的余额,扣200,对应的SQL应该是:
1
|
UPDATE user SET balance = balance - 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("balance = balance - 200") // 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
12
13
|
@Test
void testLambdaQueryWrapper() {
// 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.lambda()
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
// 2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
|
自定义SQL
在演示UpdateWrapper的案例中,我们在代码中编写了更新的SQL语句:

这种写法在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。
如下:
1
2
3
4
5
6
7
8
|
<update id="updateBalanceByIds">
UPDATE user
SET balance = balance - #{amount}
WHERE id IN
<foreach collection="ids" separator="," item="id" open="(" close=")">
#{id}
</foreach>
</update>
|
这实在是太麻烦了。假如查询条件更复杂,动态SQL的编写也会更加复杂。
所以,MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL
上述案例在MybatisPlus中就可以写成:
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
5
6
7
8
9
10
11
12
|
package com.itheima.mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Param;
public interface UserMapper extends BaseMapper<User> {
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}
|
在Mapper方法参数中用@Param声明的wrapper变量名称,必须是ew
可以直接调用mp提供的常量类,写成@Param(Constants.WRAPPER)
Service接口
MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。 通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以分为以下几类:
save:新增
remove:删除
update:更新
get:查询单个结果
list:查询集合结果
count:计数
page:分页查询
CRUD
新增:

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

removeById:根据id删除
removeByIds:根据id批量删除(用in方式删除,适合id数量少时)
removeByMap:根据Map中的键值对为条件删除
remove(Wrapper<T>):根据Wrapper条件删除
removeBatchByIds:根据id批量删除(用批处理方式删除,适合id数量多时,比如1000个以上)
修改:

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

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

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

count():统计所有数量
count(Wrapper<T>):统计符合Wrapper条件的数据数量
基本用法
要想使用MybatisPlus提供的通用service接口,需要自己的接口和实现类分别继承mp提供的接口及其实现类
先定义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
4
5
6
7
8
9
10
11
|
package com.itheima.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.service.IUserService;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
|
案例练习
要求:实现下面4个接口
| 编号 |
接口 |
请求方式 |
请求路径 |
请求参数 |
返回值 |
| 1 |
新增用户 |
POST |
/users |
用户表单实体 |
无 |
| 2 |
删除用户 |
DELETE |
/users/{id} |
用户id |
无 |
| 3 |
根据id查询用户 |
GET |
/users/{id} |
用户id |
用户VO |
| 4 |
根据id批量查询 |
GET |
/users |
用户id集合 |
用户VO集合 |
| 5 |
根据id扣减余额 |
PUT |
/users/{id}/deduction/{money} |
用户id、扣减金额 |
无 |
在项目中引入几个依赖:
1
2
3
4
5
6
7
8
9
10
11
|
<!--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>
|
配置swagger信息:
1
2
3
4
5
6
7
8
9
10
11
12
|
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.itheima.mp.controller
|
然后,接口需要两个实体:
- UserFormDTO:代表新增时前端传入的用户表单
- UserVO:代表查询的返回结果
UserFormDTO:
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
|
package com.itheima.mp.domain.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("注册手机号")
private String phone;
@ApiModelProperty("详细信息,JSON风格")
private String info;
@ApiModelProperty("账户余额")
private Integer balance;
}
|
UserVO:
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
|
package com.itheima.mp.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
@ApiModelProperty("用户id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("详细信息")
private String info;
@ApiModelProperty("使用状态(1正常 2冻结)")
private Integer status;
@ApiModelProperty("账户余额")
private Integer balance;
}
|
编写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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
package com.itheima.mp.controller;
import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.dto.UserFormDTO;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "用户管理接口")
@RequiredArgsConstructor // 只会为带有final的属性创建构造方法
@RestController
@RequestMapping("users")
public class UserController {
private final IUserService userService;
@PostMapping
@ApiOperation("新增用户")
public void saveUser(@RequestBody UserFormDTO userFormDTO){
// 1.转换DTO为PO
User user = BeanUtil.copyProperties(userFormDTO, User.class);
// 2.新增
userService.save(user);
}
@DeleteMapping("/{id}")
@ApiOperation("删除用户")
public void removeUserById(@PathVariable("id") Long userId){
userService.removeById(userId);
}
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryUserById(@PathVariable("id") Long userId){
// 1.查询用户
User user = userService.getById(userId);
// 2.处理vo
return BeanUtil.copyProperties(user, UserVO.class);
}
@GetMapping
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){
// 1.查询用户
List<User> users = userService.listByIds(ids);
// 2.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
}
|
上述4个接口都直接在controller即可实现,无需编写任何service代码,非常方便。
不过,一些带有业务逻辑的接口则需要在service中自定义实现了。
例如第5个接口:
| 编号 |
接口 |
请求方式 |
请求路径 |
请求参数 |
返回值 |
| 5 |
根据id扣减余额 |
PUT |
/users/{id}/deduction/{money} |
用户id、扣减金额 |
无 |
这看起来是个简单修改功能,只要修改用户余额即可。但这个业务包含一些业务逻辑处理:
这些业务逻辑都要在service层来做,另外更新余额需要自定义SQL,要在mapper中来实现。因此,除了要编写controller以外,具体的业务还要在service和mapper中编写。
首先在UserController中定义一个方法:
1
2
3
4
5
|
@PutMapping("{id}/deduction/{money}")
@ApiOperation("扣减用户余额")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money") Integer money){
userService.deductBalance(id, money);
}
|
UserService接口:
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> {
void deductBalance(Long id, Integer money);
}
|
UserServiceImpl实现类:
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
|
package com.itheima.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public void deductBalance(Long id, Integer money) {
// 1.查询用户
User user = getById(id);
// 2.判断用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常");
}
// 3.判断用户余额
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足");
}
// 4.扣减余额
baseMapper.deductMoneyById(id, money);
}
}
|
最后是mapper:
1
2
|
@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
void deductMoneyById(@Param("id") Long id, @Param("money") Integer money);
|
Lambda
IService中还提供了Lambda功能来简化复杂查询及更新功能。
查询
案例一:实现一个根据复杂条件查询用户的接口,查询条件如下:
- name:用户名关键字,可以为空
- status:用户状态,可以为空
- minBalance:最小余额,可以为空
- maxBalance:最大余额,可以为空
可以理解成一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断。
不基于Lambda的查询:
首先需要定义一个查询条件实体,UserQuery实体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
|
接下来在UserController中定义一个controller方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUsers(UserQuery query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda()
.like(username != null, User::getUsername, username)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance);
// 2.查询用户
List<User> users = userService.list(wrapper);
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
|
在组织查询条件的时候,加入了 username != null 这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的<if>标签。这样就实现了动态查询条件效果了。
不过,上述条件构建的代码有点麻烦了。 因此Service中对LambdaQueryWrapper和LambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuery和lambdaUpdate方法,减少了一个步骤
基于Lambda的查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUsers(UserQuery query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
// 2.查询用户
List<User> users = userService.lambdaQuery()
.like(username != null, User::getUsername, username)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance)
.list();
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
|
可以发现lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个list(),这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用list(),可选的方法有:
.one():最多1个结果
.list():返回集合结果
.count():返回计数结果
.exist():返回查询内容是否存在
- ……
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
19
20
21
22
|
@Override
@Transactional // 保障并发场景下的数据一致性
public void deductBalance(Long id, Integer money) {
// 1.查询用户
User user = getById(id);
// 2.校验用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
// 3.校验余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
// 4.扣减余额 update tb_user set balance = balance - ?
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance) // 更新余额
.set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status
.eq(User::getId, id) // where id = #{id}
.eq(User::getBalance, user.getBalance()) // 乐观锁,提高并发安全性
.update();
}
|
批量新增
IService中的批量新增功能使用起来非常方便,但得确定它的性能如何
所以批量插入10万条数据对比普通for循环插入和IService批量插入的性能
首先测试普通for循环逐条插入数据:
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;
}
|
然后再试试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));
}
|
我的电脑前者耗时:2分4秒,后者耗时:24秒
一般情况下,mp的效率会比普通for循环快得多,但还是需要二十几秒,是否可以更快呢?
答案是可以。想要得到最佳性能,最好是将多条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(从3.1.13版本开始),默认是false
它会把你合并的一条sql语句拆成一条条单独的sql语句导致性能下降
所以需要修改连接参数,将其配置为true
修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true:
1
2
3
4
|
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
|
修改完成后再次执行mp的批处理,发现效率进一步地提升了,我的耗时变成了13秒多,提升幅度会随着数据量的提升而提升
拓展功能
代码生成
插件下载
在使用MybatisPlus以后,基础的Mapper、Service、PO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO、Mapper、Service等相关代码。只不过代码生成器同样要编码使用,也很麻烦。
这里推荐大家使用一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。

然后重启Idea即可使用
使用方法
刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。
首先需要配置数据库地址,在顶部菜单找到工具-Config Database

在弹出的窗口中填写数据库连接的基本信息:
然后再点击工具-Code Generator
在弹出的表单中填写信息:

点击右下角代码生成后就会按照勾选内容生成代码
静态工具
有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:

练习-1
需求:改造根据id用户查询的接口,查询用户的同时返回用户收货地址列表
首先,添加一个收货地址的VO对象:
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
|
package com.itheima.mp.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "收货地址VO")
public class AddressVO{
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("用户ID")
private Long userId;
@ApiModelProperty("省")
private String province;
@ApiModelProperty("市")
private String city;
@ApiModelProperty("县/区")
private String town;
@ApiModelProperty("手机")
private String mobile;
@ApiModelProperty("详细地址")
private String street;
@ApiModelProperty("联系人")
private String contact;
@ApiModelProperty("是否是默认 1默认 0否")
private Boolean isDefault;
@ApiModelProperty("备注")
private String notes;
}
|
改造原本的UserVO,新增一个收货地址集合
1
2
|
@ApiModelProperty("收货地址")
private List<AddressVO> addresses;
|
修改UserController中根据id查询用户的业务接口:
1
2
3
4
5
6
|
@GetMapping("/{id}")
@ApiOperation("根据id查询用户及其地址")
public UserVO queryUserById(@PathVariable("id") Long userId){
// 基于自定义service方法查询
return userService.queryUserAndAddressById(userId);
}
|
在IUserService中定义方法:
1
|
UserVO queryUserAndAddressById(Long userId);
|
然后,在UserServiceImpl中实现该方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Override
public UserVO queryUserAndAddressById(Long userId) {
// 1.查询用户
User user = getById(userId);
if (user == null) {
return null;
}
// 2.查询收货地址
List<Address> addresses = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, userId)
.list();
// 3.处理vo
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
if (CollUtil.isNotEmpty(addresses)){
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
}
return userVO;
}
|
练习-2
需求:改造根据id批量查询用户的接口,使得在查询用户的同时,查询出用户对应的所有地址
修改UserController中根据id批量查询用户的业务接口:
1
2
3
4
5
|
@GetMapping
@ApiOperation("根据id批量查询用户及其地址")
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){
return userService.queryUserAndAddressByIds(ids);
}
|
在IUserService中定义方法:
1
|
List<UserVO> queryUserAndAddressByIds(List<Long> ids);
|
然后,在UserServiceImpl中实现该方法:
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
|
@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
// 1.查询用户
List<User> userList = listByIds(ids);
if (CollUtil.isEmpty(userList)) {
return Collections.emptyList();
}
// 2.查询地址
// 2.1 获取用户id集合
List<Long> idList = userList.stream().map(User::getId).collect(Collectors.toList());
// 2.2 根据用户id查询地址
List<Address> addressList = Db.lambdaQuery(Address.class)
.in(Address::getUserId, idList)
.list();
// 2.3 转换地址VO
List<AddressVO> addressVOList = BeanUtil.copyToList(addressList, AddressVO.class);
// 2.4 将地址集合分组,相同用户的地址放入一个集合中
// key:用户id, value:地址集合
Map<Long, List<AddressVO>> addressMap = new HashMap<>();
if (CollUtil.isNotEmpty(addressVOList)){
addressMap = addressVOList.stream()
.collect(Collectors.groupingBy(AddressVO::getUserId));
}
// 3.转换VO返回
List<UserVO> userVOList = new ArrayList<>(userList.size());
for (User user : userList) {
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
userVO.setAddresses(addressMap.get(user.getId()));
userVOList.add(userVO);
}
return userVOList;
}
|
逻辑删除
对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为true
- 查询时过滤掉标记为true的数据
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。
为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。
注意:只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
要在application.yml中配置逻辑删除字段:
1
2
3
4
5
6
|
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名,字段可以是Boolean、Integer
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
开启了逻辑删除功能以后,就可以像普通删除一样做CRUD,基本不用额外考虑代码逻辑问题。
注意: 逻辑删除本身也有自己的问题,比如:
- 会导致数据库表垃圾数据越来越多,从而影响查询效率
- SQL中全都需要对逻辑删除字段做判断,影响查询效率
枚举处理器
User类中有一个用户状态字段:
1
2
3
4
|
/**
* 使用状态(1正常 2冻结)
*/
private Integer status;
|
像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举与Integer转换,非常麻烦。
因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。
定义枚举
定义一个用户状态的枚举:
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.itheima.mp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
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;
}
}
|
然后把User类中的status字段改为UserStatus 类型:
1
2
3
4
|
/**
* 使用状态(1正常 2冻结)
*/
private UserStatus status;
|
同时也要把UserVO类中的status字段改为UserStatus 类型:
1
2
|
@ApiModelProperty("使用状态(1正常 2冻结)")
private UserStatus status;
|
要让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
6
7
8
|
{
"id": 1,
"username": "Jack",
"info": "{\"age\": 20, \"intro\": \"佛系青年\", \"gender\": \"male\"}",
"status": "NORMAL",
"balance": 1600,
"addresses": ……(略)
}
|
发现查询出的User类的status字段是枚举类型的名字
如果希望让查询出的结果更加直观,可以在枚举类中在想返回的内容上加@JsonValue注解修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结")
;
@EnumValue
private final int value;
@JsonValue // 希望返回结果中显示 正常/冻结
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
|
此时查询的返回值就变成了:
1
2
3
4
5
6
7
8
|
{
"id": 1,
"username": "Jack",
"info": "{\"age\": 20, \"intro\": \"佛系青年\", \"gender\": \"male\"}",
"status": "正常",
"balance": 1600,
"addresses": ……(略)
}
|
JSON处理器
数据库的user表中有一个info字段,是JSON类型:
格式像这样:
1
|
{"age": 20, "intro": "佛系青年", "gender": "male"}
|
而目前User实体类中却是String类型:
这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map或者实体类。
而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。
因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器。
定义实体
定义一个单独实体类来与info字段的属性匹配:
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package com.itheima.mp.domain.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
|
使用类型处理器
将User类的info字段修改为UserInfo类型,并声明类型处理器(也要改UserVO,不过不用声明类型处理器):
同时,在User类上添加一个注解,声明自动映射:
使用查询用户接口测试
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"id": 1,
"username": "Jack",
"info": {
"age": 20,
"intro": "佛系青年",
"gender": "male"
},
"status": "正常",
"balance": 1600,
"addresses": ……(略)
}
|
info字段明显比之前清晰多了
如果修改后启动项目报错,可以试试把最初提供的com.itheima.mp.mapper.UserMapper中的saveUser(User user)方法及其映射方法注释掉
配置加密
目前我们配置文件中的很多参数都是明文,如果开发人员发生流动,很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。
以数据库的用户名和密码为例。
生成秘钥
首先,我们利用AES工具生成一个随机秘钥,然后对用户名、密码加密:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package com.itheima.mp;
import com.baomidou.mybatisplus.core.toolkit.AES;
import org.junit.jupiter.api.Test;
class MpDemoApplicationTests {
@Test
void contextLoads() {
// 生成 16 位随机 AES 密钥
String randomKey = AES.generateRandomKey();
System.out.println("randomKey = " + randomKey);
// 利用密钥对用户名加密
String username = AES.encrypt("root", randomKey);
System.out.println("username = " + username);
// 利用密钥对用户名加密
String password = AES.encrypt("MySQL123", randomKey);
System.out.println("password = " + password);
}
}
|
打印结果如下:
1
2
3
|
randomKey = 6234633a66fb399f
username = px2bAbnUfiY8K/IgsKvscg==
password = FGvCSEaOuga3ulDAsxw68Q==
|
修改配置
修改application.yaml文件,把jdbc的用户名、密码修改为刚刚加密生成的密文:
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: mpw:QWWVnk1Oal3258x5rVhaeQ== # 密文要以 mpw:开头
password: mpw:EUFmeH3cNAzdRGdOQcabWg== # 密文要以 mpw:开头
|
测试
在启动项目的时候,需要把刚才生成的秘钥添加到启动参数中,像这样:
–mpw.key=6234633a66fb399f
单元测试的时候不能添加启动参数,所以要在测试类的注解上配置:
然后随意运行一个单元测试,可以发现数据库查询正常。
插件功能
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor:自动分页
TenantLineInnerInterceptor:多租户
DynamicTableNameInnerInterceptor:动态表名
OptimisticLockerInnerInterceptor:乐观锁
IllegalSQLInnerInterceptor:sql 性能规范
BlockAttackInnerInterceptor:防止全表更新与删除
注意: 使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:
- 多租户,动态表名
- 分页,乐观锁
- sql 性能规范,防止全表更新与删除
分页插件
在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IService和BaseMapper中的分页方法都无法正常起效,所以必须配置分页插件。
配置分页插件
在项目中新建一个配置类:
其代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package com.itheima.mp.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
|
测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Test
void testPageQuery() {
int pageNum = 1, pageSize = 5;
// 1.分页查询
// 1.1 分页条件:new Page()的两个参数分别是:页码、每页大小
Page<User> page = new Page<>(pageNum, pageSize);
// 1.2 设置排序条件
page.addOrder(new OrderItem("balance", true)); // true是升序,false是降序
// 1.3 查询
Page<User> p = userService.page(page);
// 2.总条数
System.out.println("total = " + p.getTotal());
// 3.总页数
System.out.println("pages = " + p.getPages());
// 4.数据
List<User> records = p.getRecords();
records.forEach(System.out::println);
}
|
通用分页实体
现在要实现一个用户分页查询的接口,接口规范如下:
| 参数 |
说明 |
| 请求方式 |
GET |
| 请求路径 |
/users/page |
| 请求参数 |
 |
| 返回值 |
 |
| 特殊说明 |
如果排序字段为空,默认按照更新时间排序排序字段不为空,则按照排序字段排序 |
这里需要定义3个实体:
UserQuery:分页查询条件的实体,包含分页、排序参数、过滤条件
PageDTO:分页结果实体,包含总条数、总页数、当前页数据
UserVO:用户页面视图实体
实体
UserQuery
UserQuery之前已经定义过了,并且其中已经包含了过滤条件,具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
|
其中缺少的仅仅是分页条件,而分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。因此建议将分页查询条件单独定义为一个PageQuery实体:
PageQuery是前端提交的查询参数,一般包含四个属性:
pageNo:页码
pageSize:每页数据条数
sortBy:排序字段
isAsc:是否升序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Long pageNo;
@ApiModelProperty("每页大小")
private Long pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}
|
然后,让UserQuery继承这个实体:
1
|
public class UserQuery extends PageQuery { …… }
|
PageDTO
分页实体PageDTO:
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.itheima.mp.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<T> list;
}
|
UserVO
UserVO就用之前定义过的那个
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
|
package com.itheima.mp.domain.vo;
import com.itheima.mp.domain.po.UserInfo;
import com.itheima.mp.enums.UserStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
@ApiModelProperty("用户id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("详细信息")
private UserInfo info;
@ApiModelProperty("使用状态(1正常 2冻结)")
private UserStatus status;
@ApiModelProperty("账户余额")
private Integer balance;
@ApiModelProperty("收货地址")
private List<AddressVO> addresses;
}
|
开发接口
在UserController中定义分页查询用户的接口:
1
2
3
4
5
|
@ApiOperation("根据条件分页查询用户")
@GetMapping("/page")
public PageDTO<UserVO> queryUsersByPage(UserQuery query){
return userService.queryUsersByPage(query);
}
|
然后在IUserService中创建queryUsersByPage方法:
1
|
PageDTO<UserVO> queryUsersByPage(UserQuery query);
|
在UserServiceImpl中实现该方法:
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
|
@Override
public PageDTO<UserVO> queryUsersByPage(UserQuery query) {
// 1.构建查询条件
// 1.1 分页条件
Long pageNum = query.getPageNo(); // 页码
Long pageSize = query.getPageSize(); // 每页大小
Page<User> page = new Page<>(pageNum, pageSize);
// 1.2 排序条件
if (StrUtil.isNotBlank(query.getSortBy())) {
page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
} else {
// 为空,则按更新时间排序
page.addOrder(new OrderItem("update_time", false));
}
// 2.分页查询
String name = query.getName();
Integer status = query.getStatus();
Page<User> p = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
// 3.封装VO结果
PageDTO<UserVO> pageDTO = new PageDTO<>();
// 3.1 设置总条数
pageDTO.setTotal(p.getTotal());
// 3.2 设置总页数
pageDTO.setPages(p.getPages());
// 3.3 设置当前页数据
if (CollUtil.isEmpty(p.getRecords())) {
pageDTO.setList(Collections.emptyList());
} else {
pageDTO.setList(BeanUtil.copyToList(p.getRecords(), UserVO.class));
}
// 4.返回
return pageDTO;
}
|
测试接口,确认可以正常使用,符合需求
改造PageQuery实体
在刚才的代码中,从PageQuery到MybatisPlus的Page之间转换的过程还是比较麻烦的。
完全可以在PageQuery这个实体中定义一个工具方法,将PageQuery对象转为MybatisPlus中的Page对象,简化开发。
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
|
package com.itheima.mp.domain.query;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Long pageNo = 1L; // 默认第一页
@ApiModelProperty("每页大小")
private Long pageSize = 5L; // 默认每页5条数据
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
public <T> Page<T> toMpPage(OrderItem ... orders){
// 1.分页条件
Page<T> p = Page.of(pageNo, pageSize);
// 2.排序条件
// 2.1.先看前端有没有传排序字段
if (sortBy != null) {
p.addOrder(new OrderItem(sortBy, isAsc));
return p;
}
// 2.2.再看有没有手动指定排序字段
if(orders != null){
p.addOrder(orders);
}
return p;
}
public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
}
public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
return toMpPage("create_time", false);
}
public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
return toMpPage("update_time", false);
}
}
|
改造PageDTO实体
同理,在PageDTO中定义一个工具方法,将MybatisPlus中的Page对象转为PageDTO对象,简化开发。
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
|
package com.itheima.mp.domain.dto;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "分页结果")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<T> list;
/**
* 返回空分页结果
* @param p MybatisPlus的分页结果
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> empty(Page<P> p){
return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
}
/**
* 将MybatisPlus分页结果转为 VO分页结果
* @param p MybatisPlus的分页结果
* @param voClass 目标VO类型的字节码
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {
// 1.非空校验
List<P> records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> vos = BeanUtil.copyToList(records, voClass);
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
/**
* 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
* @param p MybatisPlus的分页结果
* @param convertor PO到VO的转换函数
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {
// 1.非空校验
List<P> records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
}
|
简化后的业务代码
最终,业务层的代码可以简化为:
1
2
3
4
5
6
7
8
9
|
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, UserVO.class);
}
|
如果是希望自定义PO到VO的转换过程,可以这样做:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, user -> {
// 拷贝属性到VO
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 用户名脱敏
String username = vo.getUsername();
vo.setUsername(username.substring(0, username.length() - 2) + "**");
return vo;
});
}
|