MybatisPlus

介绍了MybatisPlus在SpringBoot中的使用

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:

image-20251210213625016

所以在自己的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条件

image-20251211200812342

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

image-20251211200837739

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语句:

img

这种写法在某些企业也是不允许的,因为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

新增

img

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

img

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

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

基本用法

要想使用MybatisPlus提供的通用service接口,需要自己的接口和实现类分别继承mp提供的接口及其实现类

image-20251211213000860

先定义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中对LambdaQueryWrapperLambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuerylambdaUpdate方法,减少了一个步骤

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

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

img

然后重启Idea即可使用

使用方法

刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。

首先需要配置数据库地址,在顶部菜单找到工具-Config Database

image-20251212163417044

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

img

然后再点击工具-Code Generator

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

img

点击右下角代码生成后就会按照勾选内容生成代码

静态工具

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

img

练习-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提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换

定义枚举

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

img

代码如下:

 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注解来标记枚举属性:

img

配置枚举处理器

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类型:

img

格式像这样:

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

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

img

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

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

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

定义实体

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

img

代码如下:

 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,不过不用声明类型处理器):

img

同时,在User类上添加一个注解,声明自动映射:

img

使用查询用户接口测试

 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

单元测试的时候不能添加启动参数,所以要在测试类的注解上配置:

img

然后随意运行一个单元测试,可以发现数据库查询正常。

插件功能

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

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

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

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

分页插件

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

配置分页插件

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

img

其代码如下:

 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
请求参数 image-20251213120637494
返回值 image-20251213120651621
特殊说明 如果排序字段为空,默认按照更新时间排序排序字段不为空,则按照排序字段排序

这里需要定义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实体:

img

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:

img

代码如下:

 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实体

在刚才的代码中,从PageQueryMybatisPlusPage之间转换的过程还是比较麻烦的。

完全可以在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;
    });
}
本站于2025年3月26日建立
使用 Hugo 构建
主题 StackJimmy 设计