Mybatis-Plus


快速开始


  • 引入 MybatisPlus 的起步依赖

    • MyBatisPlus官方提供了 starter ,其中 集成了 Mybatis 和 MybatisPlus 的所有功能,并且 实现了自动装配效果。因此我们可以 用MybatisPlus的starter 代替 Mybatis的starter
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
    </dependency>
  • 定义 Mapper

    • 自定义的 Mapper 继承 MybatisPlus 提供的 BaseMapper 接口
    1
    2
    3
    4
    // BaseMapper 要指定实体类的类型,指明操作对象
    public interface UserMapper extends BaseMapper<User> {

    }

    BaseMapper的CURD方法


常用注解


MyBatisPlus 通过扫描实体类,并基于反射获取实体类信息作为数据库表信息

数据库对应的Pojo

  • 泛型中的 User 就是与数据库对应的 PO
  • MybatisPlus 就是根据 PO实体 的信息来推断出 表的信息,从而生成SQL的。默认情况下:
    1. MybatisPlus 会把 PO实体 的 类名驼峰转下划线 作为 表名
    2. MybatisPlus 会把 PO实体 的 所有变量名驼峰转下划线 作为 表的字段名,并根据变量类型推断 字段类型
    3. MybatisPlus 会把名为 id 的字段作为 主键

@TableName


  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类

eg:

1
2
3
4
5
@TableName("user")
public class User {
private Long id;
private String name;
}

@TableId


  • 描述:主键注解,标识实体类中的主键字段
  • 使用位置:实体类的主键字段

eg:

1
2
3
4
5
6
7
@TableName("user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
// 这里type一定要指定出来,否则默认雪花算法
private Long id;
private String name;
}

@TableId 支持两个属性:

@TableId两个属性

IdType 支持的类型:这里只给出比较常见的

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

@TableField


  • 描述:普通字段注解

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@TableName("user")
public class User {
@TableId
private Long id;
@TableField("username")
private String name;
private Integer age;
@TableField("isMarried")
private Boolean isMarried;
@TableField("`concat`") // 因为数据库中有一个函数是concat,冲突了
private String concat;

@TableField(exist = false) // 这里不是数据库字段
private String address;
}

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

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

常见配置


MyBatisPlus 的配置项 继承了MyBatis原生配置 和 一些自己特有的配置

1
2
3
4
5
6
7
8
9
10
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po # 别名扫描包
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,默认值
configuration:
map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
cache-enabled: false # 是否开启二级缓存
global-config:
db-config:
id-type: assign_id # id为雪花算法生成
update-strategy: not_null # 更新策略:只更新非空字段

总结:

MyBatisPlus 使用的 基本流程 是:

  1. 引入起步依赖
  2. 自定义 Mapper 基础 BaseMapper
  3. 在实体类上添加注解声明 表信息
  4. 在 application.yml 中根据需要添加配置

条件构造器


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

BaseMapper

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

Wrapper继承关系

AbstractWrapper

QueryWrapper

UpdateWrapper


QueryWrapper


无论是修改、删除、查询,都可以使用 QueryWrapper 来构建查询条件

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 查询出名字中带o的,存款大于等于1000元的人

@Test
void testQueryWrapper() {
// 1. 构造查询条件
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);

// 2. 查询
List<User> users = userMapper.selectList(wrapper);

// 3. 遍历
users.forEach(System.out::println);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 更新用户名为jack的用户的余额为2000

@Test
void testUpdateByQueryWrapper() {
// 1. 需要更新的数据
User user = new User();
user.setBalance(2000);

// 2. 需要更新的条件
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username","jack");

// 3. 执行更新
userMapper.update(user, wrapper);
}

UpdateWrapper


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

eg:

1
2
3
4
5
6
7
8
9
10
11
// 更新id为 1, 2, 4的用户的余额,扣 200,对应的SQL应该是
// update tb_user set balance = balance - 200 where id in (1, 2, 4);

@Test
void testUpdateWrapper() {
List<Long> ids = Arrays.asList(1L, 2L, 4L);
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.setSql("balance = balance - 200")
.in("id", ids);
userMapper.update(null, wrapper);
}

LambdaQueryWrapper


无论是 QueryWrapper 还是 UpdateWrappe r在构造条件的时候都需要 写死字段名称,会出现字符串魔法值

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

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
void testLambdaQueryWrapper() {
// 1. 构造查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);

// 2. 查询
List<User> users = userMapper.selectList(wrapper);

// 3. 遍历
users.forEach(System.out::println);
}

总结:

条件构造器的用法:

  • QueryWrapper 和 LambdaQueryWrapper 通常用来构建 select 、delete 、update 的 where 条件部分
  • UpdateWrapper 和 LambdaUpdateWrapper 通常只有在 set 语句比较特殊才使用
  • 尽量使用 LambdaQueryWrapper 和 LambdaUpdateWrapper,避免硬编码

自定义SQL


利用 MyBatisPlus 的 Wrapper 来 构建复杂的 Where 条件,然后自己定义 SQL 语句中剩下的部分

自定义SQL的使用

步骤:

  1. 基于 Wrapper 构建 where 条件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 举例场景:将id在指定范围的用户(例如1、2、4 )的余额扣减指定值

    List<Long> ids = Arrays.asList(1L, 2L, 4L);
    int amount = 200;
    // 1. 构建条件
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);

    // 2. 自定义SQL方法调用
    userMapper.updateBalanceByIds(wrapper, amount);
  2. 在 mapper 方法参数中用 Param 注解声明 wrapper 变量名称,必须是 ew

    1
    2
    3
    void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);

    // Constants.WRAPPER 就是 ew
  3. 自定义SQL,并使用 Wrapper 条件

    1
    2
    3
    4
    5
    <update id="updateBalanceByIds">
    UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
    </update>

    <!-- ${ew.customSqlSegment} 这里指代了where条件部分 -->

Service 接口


MybatisPlus不仅提供了 BaseMapper ,还提供了通用的 Service 接口及默认实现,封装了一些常用的 service模板方法

通用接口为 IService ,默认实现为 ServiceImpl ,其中封装的方法可以分为以下几类:

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

新增

新增save

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

删除

删除remove

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

修改

修改update

  • 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 条件的 数据数量

继承关系


基本用法

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

  1. 自定义 Service 接口 继承 IService 接口

    1
    2
    public interface IUserService extends IService<User> {
    }
  2. 自定义 Service 实现类,实现自定义接口并继承 ServiceImpl 类

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

eg:

需求:基于 Restful 风格实现下面的接口

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// 控制层

@RestController
@RequestMapping("/users")
@Api(tags = "用户管理接口")
@RequiredArgsConstructor // 只对有需要的构造函数进行注入
public class UserController {

// Spring推荐用构造函数注入
// @Autowired
private final IUserService userService;

// public UserController(IUserService userService) {
// this.userService = userService;
// }

/**
* 新增
* @param userDTO
*/
// UserFormDTO 这种属于表单实体,会以Json的格式提交,@RequestBody 注解可以接收这种Json形式的参数
@PostMapping
@ApiOperation("新增用户接口")
public void saveUser(@RequestBody UserFormDTO userDTO) {
// 1. 把DTO拷贝到PO
User user = BeanUtil.copyProperties(userDTO, User.class);

// 2. 再去做新增
userService.save(user);
}

/**
* 根据id删除用户
* @param id
*/
@DeleteMapping("{id}")
@ApiOperation("删除用户接口")
public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
userService.removeById(id);
}

/**
* 根据id查询用户
* @param id
*/
@GetMapping("{id}")
@ApiOperation("根据id查询用户接口")
public UserVO QueryUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
// 1. 查询用户PO
User user = userService.getById(id);

// 2. 把用户PO拷贝到VO
return BeanUtil.copyProperties(user, UserVO.class);
}

/**
* 根据id批量查询用户
* @param ids
* @return
*/
// @RequestParam 这种一般是在url后面拼接
@GetMapping
@ApiOperation("根据id批量查询用户接口")
public List<UserVO> QueryUserById(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {
// 1. 查询用户PO集合
List<User> users = userService.listByIds(ids);
// 2. 把用户PO集合拷贝到VO集合
return BeanUtil.copyToList(users, UserVO.class);
}

/**
* 根据id扣减余额
* @param id
* @param money
*/
@PutMapping("/{id}/deduction/{money}")
@ApiOperation("根据id扣减余额接口")
public void deductBalanceById(@ApiParam("用户id") @RequestParam("id") Long id
, @ApiParam("扣减的金额") @RequestParam("money") Integer money) {
userService.deductBalanceById(id, money);
}
}
1
2
3
4
5
6
7
// 持久层

public interface UserMapper extends BaseMapper<User> {

@Update("update mp.user set balance = balance - #{money} where id = #{id}")
void deductBalance(@Param("id") Long id, @Param("money") Integer money);
}
1
2
3
4
5
// 实现接口

public interface IUserService extends IService<User> {
void deductBalanceById(Long id, Integer money);
}
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
// 实现类

public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {


@Override
public void deductBalanceById(Long id, Integer money) {
// 1. 查询用户
User user = this.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 - {#money}
baseMapper.deductBalance(id, money);

}
}

Lambda

Lambda接口

LambdaQuery的查询方法使用

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

  • name:用户名关键字,可以为空
  • status:用户状态,可以为空
  • minBalance:最小余额,可以为空
  • maxBalance:最大余额,可以为空

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 控制层

/**
* 根据复杂条件查询用户接口
* @param query
* @return
*/
// Get请求的参数不用加注解,可以直接用对象接收
@GetMapping("/list")
@ApiOperation("根据复杂条件查询用户接口")
public List<UserVO> queryUsers(UserQuery query) {

// 1. 查询用户PO
List<User> queryUsers = userService.queryUsers
(query.getName(), query.getStatus(), query.getMaxBalance(), query.getMinBalance());

// 2. 把UserPOJO拷贝到UserVO相同
return BeanUtil.copyToList(queryUsers, UserVO.class);
}
1
2
3
// 实现接口

List<User> queryUsers(String name, Integer status, Integer maxBalance, Integer minBalance);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 实现类

@Override
public List<User> queryUsers(String name, Integer status, Integer maxBalance, Integer minBalance) {

// lambdaQuery() 第一个参数是判断
List<User> users = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.le(maxBalance != null, User::getBalance, maxBalance)
.ge(minBalance != null, User::getBalance, minBalance)
// .count() // 查询总数
// .page() // 分页查询
.list();// 查询多个
// .one() // 查询一个

return users;
}

LambdaUpdate的更新方法使用

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

  • 完成对用户状态校验
  • 完成对用户余额校验
  • 如果扣减后余额为0,则将用户status修改为冻结状态

eg:

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
@Override
@Transactional
public void deductBalanceById(Long id, Integer money) {
// 1. 查询用户
User user = this.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 - {#money}
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance)
.set(remainBalance == 0, User::getStatus, 2)
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance()) // 乐观锁,先比较再查询
.update(); // .update() 才是真正执行update语句

}

IService 批量新增

  1. 批处理方案:普通 for 循环 逐条插入 速度极差,不推荐

  2. MP 的批量新增,基于 预编译 的 批处理 ,性能不错

  3. 配置 JDBC 参数(在 yml 文件里),开 rewriteBatchedStatements,性能最好

    1
    rewriteBatchedStatements=true

代码生成


安装MybatisPlus插件

下一步连接

下一步连接数据库

下一步生成信息的界面


静态工具


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

DB 静态工具

需求:

  • 改造根据 id 查询用户的接口,查询用户的同时,查询出用户对应的所有地址
  • 改造根据 id 批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 // UserVo

@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;

@ApiModelProperty("用户的收货地址")
private List<AddressVO> addresses;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public UserVO queryUserAndAddressById(Long id) {
// 1. 查询用户
User user = lambdaQuery().eq(id != null, User::getId, id).one();
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常");
}

// 2. 查询地址
// 静态方法,还有一种方法是注入service
List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, id).list();

// 3. 封装VO
// 3.1 转User的PO为VO
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
// 3.2 转地址VO
if (CollUtil.isNotEmpty(addresses)) {
List<AddressVO> addressVOS = BeanUtil.copyToList(addresses, AddressVO.class);
userVO.setAddresses(addressVOS);
}

return 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
33
34
35
36
37
@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
// 1. 查询用户
List<User> users = this.listByIds(ids);
if (CollUtil.isEmpty(users)) {
throw new RuntimeException("用户为空");
}

// 2. 查询地址
// 2.1 获取用户id集合
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
// 2.2 根据用户id查询地址
List<Address> addresses = Db.lambdaQuery(Address.class)
.in(Address::getUserId, userIds)
.list();
// 2.3 转换地址VO
List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
Map<Long, List<AddressVO>> map = null;

// 2.4 梳理地址集合,对用户地址集合进行分组
if (CollUtil.isNotEmpty(addressVOList)) {
map = addressVOList
.stream()
.collect(Collectors.groupingBy(AddressVO::getUserId));
}

// 3. 转换VO返回
List<UserVO> list = new ArrayList<>(users.size());
for (User user : users) {
// 3.1 转换User的PO为VO
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
list.add(userVO);
// 3.2 转换地址VO
userVO.setAddresses(map.get(user.getId()));
}
return list;
}

逻辑删除


逻辑删除就是 基于代码逻辑 模拟删除 效果,但并 不会 真正 删除数据。

思路如下:

  • 在表中 添加一个字段 标记数据 是否 被删除
  • 当 删除数据 时把标记置为 1
  • 查询时 只查询标记为 0 的数据

举例

MybatisPlus 提供了 逻辑删除 功能,无需改变方法调用的方式,而是在底层自动修改 CRUD 的语句。只需要在 application.yaml 文件中 配置逻辑删除 的 字段名称 和 值 即可。

1
2
3
4
5
6
7
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)

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

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

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


枚举处理器


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

eg:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结")
;
// @EnumValue注解来标记枚举属性
@EnumValue
private final int value;

// @JsonValue注解标记JSON序列化时展示
@JsonValue
private final String desc;

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

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

1
2
3
4
5
6
7
// 修改之前
// 使用状态(1 正常 2 冻结)
private Integer status;


// 修改之后
private UserStatus status;

在 application.yml 中配置 全局枚举处理器 :

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

JSON处理器


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

User表

eg:

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

1
2
3
4
5
6
7
8
9
10
11
@Data
@TableName(value = "user", autoResultMap = true)
public class User {
private Long id;
private String username;

// 这里与UserInfo建立映射
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
}


插件功能


MyBatisPlus 提供的 内置拦截器 如下:

内置拦截器


分页插件


步骤:

  1. 要在 配置类config文件夹 中注册 MyBatisPlus 的核心插件,同时添加 分页插件

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

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    // 1. 初始化核心插件
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 2. 添加分页插件
    PaginationInnerInterceptor pageInterceptor = new
    PaginationInnerInterceptor(DbType.MYSQL);

    pageInterceptor.setMaxLimit(1000L); // 设置分页上线
    interceptor.addInnerInterceptor(pageInterceptor);
    return interceptor;
    }
    }
  2. 使用分页的API

    IService下面的方法

    IPage下面的子类

    这个 Page类 下面一般存储页的数量、页的大小等等

  3. 编写一个 分页查询 的测试

    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
    @Test
    void testPageQuery() {

    // 1. 查询
    int pageNo = 1, pageSize = 5;

    // 1.1 分页参数
    Page<User> page = Page.of(pageNo, pageSize);

    // 1.2 排序参数, 通过OrderItem来指定
    // 第一个参数是需要排序的元素,第二个参数是否为升序(asc)
    page.addOrder(new OrderItem("balance", false));
    page.addOrder(new OrderItem("id", true));

    // 1.3 分页查询,new Page()的两个参数分别是:页码、每页大小
    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);
    }

通用分页实体


主要是封装统一的查询条件(UserQuery)以及统一的返回值(PageDTO)

需求:遵循下面的接口规范,编写一个 UserController 接口,实现 User 的 分页查询

举例

eg:

这里需要定义3个实体:

  • UserQuery:分页查询条件的实体,包含分页、排序参数、过滤条件
  • PageDTO:分页结果实体,包含总条数、总页数、当前页数据
  • UserVO:用户页面视图实体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// UserQuery

@Data
@EqualsAndHashCode(callSuper = true) // 调用父类的方法作为我自己使用
@ApiModel(description = "用户查询条件实体")
public class UserQuery extends PageQuery{

@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// PageQuery

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {

@ApiModelProperty("页码")
private Integer pageNo;

@ApiModelProperty("页的大小")
private Integer pageSize;

@ApiModelProperty("排序字段")
private String sortBy;

@ApiModelProperty("是否升序")
private boolean isAsc;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// UserVO

@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;

@ApiModelProperty("用户的收货地址")
private List<AddressVO> addresses;
}
1
2
3
4
5
6
7
8
9
10
11
12
// PageDTO

@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<T> list;
}

控制层代码以及实现接口代码:略

在 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
40
41
42
43
44
45
46
47
48
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
String name = query.getName();
Integer maxBalance = query.getMaxBalance();
Integer minBalance = query.getMinBalance();
Integer status = query.getStatus();
// 1. 构建查询条件

// 1.1 分页条件
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());

// 1.2 排序条件
if (StrUtil.isNotBlank(query.getSortBy())) {
// 不为空
page.addOrder(new OrderItem(query.getSortBy(), false));
} else {
// 为空,按照更新时间排序
page.addOrder(new OrderItem("update_time", false));
}

// 2. 分页查询
Page<User> p = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.le(maxBalance != null, User::getBalance, maxBalance)
.ge(minBalance != null, User::getBalance, minBalance)
.page(page);

// 3. 封装VO
PageDTO<UserVO> dto = new PageDTO<>();

// 3.1 总页数
dto.setPages(p.getPages());

// 3.2 总条数
dto.setTotal(p.getTotal());

// 3.3 当前页数据
List<User> records = p.getRecords();
if (CollUtil.isEmpty(records)) {
dto.setList(Collections.emptyList());
return dto;
}

// 4. 返回结果
List<UserVO> userVOList = BeanUtil.copyToList(records, UserVO.class);
dto.setList(userVOList);
return dto;
}

改造PageQuery实体


在查询出分页结果后,数据的非空校验,数据的vo转换都是模板代码,编写起来很麻烦。

可以将其封装到 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
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {

@ApiModelProperty("页码")
private Integer pageNo = 1;

@ApiModelProperty("页的大小")
private Integer pageSize = 5;

@ApiModelProperty("排序字段")
private String sortBy;

@ApiModelProperty("是否升序")
private boolean isAsc = true;

public <T> Page<T> toMpPage(OrderItem... items) {
// 1. 分页条件
Page<T> page = Page.of(pageNo, pageSize);

// 2. 排序条件
if (StrUtil.isNotBlank(sortBy)) {
// 不为空
page.addOrder(new OrderItem(sortBy, isAsc));
} else {
// 为空,默认按照更新时间排序
page.addOrder(items);
}

return page;
}

public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
}

public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
return toMpPage(new OrderItem("create_time", false));
}

public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
return toMpPage(new OrderItem("update_time", false));
}
}

改造PageDTO实体

在查询出分页结果后,数据的非空校验,数据的vo转换都是模板代码,编写起来很麻烦。

可以将其封装到 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
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
private Long total;
private Long pages;
private List<V> 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);
}
}