Mybatis-Plus学习笔记

发布于:2025-09-14 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

一、MyBatis-Plus简介

二、MyBatisPlus使用的基本流程:

(1)引入MybatisPlus依赖,代替MyBatis依赖

(2)自定义Mapper继承BaseMapper

​编辑(3)在实体类上添加注解声明表信息

(4)在application.yml中根据需要添加配置

三、核心功能

1、条件构造器

(1)QueryWrapper

(2)UpdateWrapper

(3)基于Lambda的Wrapper

2、自定义SQL(擅长处理where更新条件)

3、IService接口

4、LambdaQuery和LambdaUpdate

lambdaUpdate实现

5、批量新增 & 批处理方案性能测试

拓展:rewriteBatchedStatements=true 和 allowMultiQueries=true 的区别

@RequiredArgsConstructor注解

四、扩展功能

1、代码生成

(1)安装插件

(2)使用步骤

(3)代码生成器配置

2、静态工具

3、逻辑删除

(1)介绍

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

(2)@TableLogic

4、枚举处理器

(1)定义枚举,标记@EnumValue

(2)配置枚举处理器

5、JSON类型处理器

(1)定义接收Json的实体类

(2)指定类型处理器

6、yaml配置加密

(1)生成密钥        

(2)修改配置

(3)配置密钥运行参数

(4)实现原理

7、自动填充字段

(1)配置自动填充处理器

(2)添加@TableField的fill属性

五、插件功能

1、分页插件

(1)引入依赖

(2)配置分页内置拦截器

(3)分页API

2、通用分页实体

(1)实体类设计

(2)开发接口

(3)改造PageDTO实体

(4)改造PageResult实体


一、MyBatis-Plus简介


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

  • 支持的数据库

  • 框架结构


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

参考文档:https://mybatis.plus/ (网站访问速度稍慢,建议直接看官网文档)

二、MyBatisPlus使用的基本流程:
 

(1)引入MybatisPlus依赖,代替MyBatis依赖


MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。

<!-- springboot2的mybatis-plus依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.9</version>
</dependency>



注意:如果是springboot3,引入的是mybatis-plus-spring-boot3-starter依赖。

<!-- springboot3的mybatis-plus依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.9</version>
</dependency>


(2)自定义Mapper继承BaseMapper


(3)在实体类上添加注解声明表信息

MybatisPlus底层通过反射,根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下(约定):

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

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

  • @TableName:用来指定表名
  • @Tableld:用来指定表中的主键字段信息
  • @TableField:用来指定表中的普通字段信息

常见注解:

(4)在application.yml中根据需要添加配置

常见配置:

三、核心功能

1、条件构造器

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

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

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

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

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

(1)QueryWrapper

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

示例:

// 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
@Test
void testQueryWrapper() {
    // 1.构建查询条件 where username like "%o%" AND balance >= 1000
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.select("id", "username", "info", "balance")
            .like("username", "o")
            .ge("balance", 1000);
    // 2.查询数据
    List<User> userList = userMapper.selectList(queryWrapper);
    userList.forEach(System.out::println);
}
// 更新用户名为jack的用户的余额为2000。
@Test
void testUpdateByQueryWrapper() {
    // 1.设置要更新的数据
    User user = new User();
    user.setBalance(2000);
    // 2.构建更新条件 where username = "Jack"
    QueryWrapper<User> queryWrapper = new QueryWrapper<User>().eq("username", "Jack");
    // 3.执行更新,user中非null字段都会作为set语句
    System.out.println(userMapper.update(user, queryWrapper) > 0);
}

(2)UpdateWrapper

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

示例:

@Test
void testUpdateWrapper() {
    List<Long> ids = List.of(1L, 2L, 4L);
    // 1.生成SQL
    UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
            .setSql("balance = balance - 200")  // SET balance = balance - 200
            .in("id", ids); // WHERE id in (1, 2, 4)
    // 2.基于UpdateWrapper中的setSql来更新
    System.out.println(userMapper.update(updateWrapper) > 0);
}

(3)基于Lambda的Wrapper
  • LambdaQueryWrapper,对应QueryWrapper
  • LambdaUpdateWrapper,对应UpdateWrapper

示例:

@Test
void testLambdaUpdateWrapper() {
    List<Long> ids = List.of(1L, 2L, 4L);
    // 1.生成SQL
    LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<User>()
            .setSql("balance = balance - 200")  // SET balance = balance - 200
            .in(User::getId, ids); // WHERE id in (1, 2, 4)
    // 2.基于UpdateWrapper中的setSql来更新
    System.out.println(userMapper.update(lambdaUpdateWrapper) > 0);
}

@Test
void testLambdaQueryWrapper() {
    // 1.构建查询条件 where username like "%o%" AND balance >= 1000
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000);
    // 2.查询数据
    List<User> userList = userMapper.selectList(queryWrapper);
    userList.forEach(System.out::println);
}

总结:

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

2、自定义SQL(擅长处理where更新条件)

  • 问题引出

把Mapper层的sql语句写在Service层了,这在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。

示例:

@Test
void testCustomSQLUpdate() {
    // 更新条件
    List<Long> ids = List.of(1L, 2L, 4L);
    int amount = 200;
    // 定义条件
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);
    // 调用自定义SQL方法
    userMapper.updateBalanceByIds(wrapper, amount);
}
  • ${ew.customSqlSegment}:为自定义SQL片段,@Param("ew")其中参数ew必须叫这个,如果忘记了也可以用baomidou包下的常量类Constants.WRAPPER,其值等于"ew"

3、IService接口

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

继承+实现

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

代码示例:

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

public interface IUserService extends IService<User> {
}



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 {
}

注意:ServiceImpl<M, T>接口的泛型参数中,M是继承了BaseMapper的Mapper接口,T是PO实体类。

4、LambdaQuery和LambdaUpdate

IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。

代码示例:

public interface IUserService extends IService<User> {
    List<User> queryUserByCondition(UserQueryDTO queryDTO);
}

// 基于Lambda查询
@Override
public List<User> queryUserByCondition(UserQueryDTO queryDTO) {
    String name = queryDTO.getName();
    Integer status = queryDTO.getStatus();
    Integer minBalance = queryDTO.getMinBalance();
    Integer maxBalance = queryDTO.getMaxBalance();
    return lambdaQuery()
            .like(name != null, User::getUsername, name)
            .eq(status != null, User::getStatus, status)
            .ge(minBalance != null, User::getBalance, minBalance)
            .le(maxBalance != null, User::getBalance, maxBalance)
            .list();
}

MP对LambdaQueryWrapper和LambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuery和lambdaUpdate方法。在组织查询条件的时候,我们加入了name != null这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的<if>标签。这样就实现了动态查询条件效果了。

MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个查询结果,list()表示查询结果返回一个List集合。可选的常用方法有:

  • one():最多1个结果
  • list():返回集合结果
  • count():返回计数结果
  • exist():返回查询的结果是否存在

lambdaUpdate实现

代码示例:

@Override
@Transactional
public void deductBalanceById(Long id, Integer money) {
    // 查询用户
    User user = getById(id);
    // 校验用户状态
    if (user == null || user.getStatus() == 2) {
        throw new RuntimeException("用户状态异常!");
    }
    // 校验余额是否充足
    if (user.getBalance() < money) {
        throw new RuntimeException("用户余额不足!");
    }
    // 扣减余额
    //baseMapper.deductBalance(id, money);
    int remainBalance = user.getBalance() - money;
    lambdaUpdate()
            .set(User::getBalance, remainBalance)   // 更新余额
            .set(remainBalance == 0, User::getStatus, 2)    // 动态判断是否更新status
            .eq(User::getBalance, user.getBalance())    // CAS乐观锁
            .eq(User::getId, id)   // 根据id扣减对应用户的余额
            .update();  // 注意:LambdaUpdate做复杂更新时,最后必须记得加上.update()进行更新操作
}

5、批量新增 & 批处理方案性能测试

需求:批量插入10万条用户数据,并作出对比。

  • 方式一:普通for循环逐条插入
  • 方式二:IService的批量插入(默认不开启 jdbc 批处理参数)
  • 方式三:开启rewriteBatchedStatements=true参数

方式一:执行结果耗时大约为551.9秒

/**
 * 10w次插入意味着10w次网络请求,耗时最慢
 */
@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;
}

方式二:执行结果耗时大约为27.6秒,打包逐条插入,从网络请求层面大大减少了耗时。

/**
 * MP批处理采用的是JDBC底层的预编译方案PreparedStatement,将1000条数据统一打包执行save一并提交到MySQL,每1000条发送一次网络请求,插入100次共发送100次网络请求
 * MP如果不加JDBC连接参数rewriteBatchedStatements=true,底层还是打包逐条插入,只不过是从网络请求数量上减少了耗时
 * 而加上了MySQL的这个开启批处理参数后,MP调用的JDBC底层的批处理才能真正变成一次性批量插入多条数据
 */
@Test
void testSaveBatch() {
    // 因为一次性new 10万条数据占用内存太多,并且向数据库请求的数据包有上限大小限制(一次网络传输的数据量是有限的)
    // 所以我们每次批量插入1000条件,插入100次即10万条数据
    // 准备一个容量为1000的集合
    List<User> list = new ArrayList<>(1000);
    long b = System.currentTimeMillis();
    for (int i = 1; i <= 100000; i++) {
        // 添加一个user
        list.add(buildUser(i));
        // 每1000条批量插入一次
        if (i % 1000 == 0) {
            // 批量插入
            userService.saveBatch(list);
            // 清空集合,准备下一批数据
            list.clear();
        }
    }
    long e = System.currentTimeMillis();
    System.out.println("耗时:" + (e - b));
}

方式三:开启参数后,测试耗时大约为6.5秒

利用MP的jdbc批处理,只不过MySQL本身默认没有开启这个批处理参数rewriteBatchedStatements=true,该参数在MySQL 3.1.13版本开始引入,默认值为false不开启。

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456

批处理方案总结:

  • 普通for循环逐条插入速度极差,不推荐
  • MP的批量新增,基于预编译的批处理,性能不错
  • 配置jdbc参数,开启rewriteBatchedStatements,性能最好
拓展:rewriteBatchedStatements=true 和 allowMultiQueries=true 的区别
  • rewriteBatchedStatements是重写sql语句达到发送一次sql的请求效果;allowMultiQueries是在mapper.xml中使用;分号分隔多条sql,允许多条语句一起发送执行的。
  • 对于insert批处理操作,开启rewriteBatchedStatements=true,驱动则会把多条sql语句重写成一条sql语句然后再发出去;而对于update和delete批处理操作,开启allowMultiQueries=true,驱动所做的事就是把多条sql语句累积起来再一次性发出去。

@RequiredArgsConstructor注解

@RequiredArgsConstructor 是 Project Lombok 库提供的一个注解,它用于在编译时自动生成一个包含所有 “必需”字段 的构造函数。

哪些字段被认为是“必需的”?

  1. 所有被 final 关键字修饰的字段:这些字段必须在对象创建时被初始化。

  2. 所有被 @NonNull 注解标记且未初始化的字段:这些字段不能为 null,因此也必须在构造函数中初始化。

对于普通的、非 final 且没有 @NonNull 约束的字段,不会被包含在这个生成的构造函数中。

四、扩展功能

1、代码生成

(1)安装插件

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

(2)使用步骤

我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到other,选择Config Database,在弹出的窗口中填写数据库连接的基本信息:

点击OK保存。然后再次点击Idea顶部菜单中的other,然后选择Code Generator,在弹出的表单中填写信息:

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

(3)代码生成器配置

如果不想用图形化界面方式配置生成代码,使用MyBatis-Plus官网提供的代码生成器模板也是可以的,但需要自己填写配置信息。

因为MP代码生成更新迭代速度很快,若本文的API被弃用,请以官网最新版本API为准:

MyBatis-Plus新代码生成器:https://baomidou.com/guides/new-code-generator/

代码生成器配置:https://baomidou.com/reference/new-code-generator-configuration/

  • 引入依赖
<!-- MP代码生成器 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.9</version>
</dependency>
  • 代码生成模板配置示例
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;

import java.util.Collections;
import java.util.List;

public class CodeGenerator {
 
 
    /**
     * 数据库链接地址
     **/
    private static final String JDBC_URL_MAN = "jdbc:mysql://xxxxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8";
    /**
     * 数据库登录账号
     **/
    private static final String JDBC_USER_NAME = "xx";
    /**
     * 数据库登录密码
     **/
    private static final String JDBC_PASSWORD = "xxxx";
 
 
    public static void main(String[] args) {
        String dir = "\\xx\\xxx";
        String tablePrefix = "tb_";
        List<String> tables = List.of("tb_user");
 
        FastAutoGenerator.create(JDBC_URL_MAN, JDBC_USER_NAME, JDBC_PASSWORD)
                .globalConfig(builder -> {
                    builder.author("Aizen")                                                           // 作者
                            .outputDir(System.getProperty("user.dir") + dir + "\\src\\main\\java")    // 输出路径(写到java目录)
                            .enableSwagger()                                                          // 开启swagger
                            .commentDate("yyyy-MM-dd");                                       // 设置注释日期格式,默认值: yyyy-MM-dd
 
                })
                .packageConfig(builder -> {
                    builder.parent("com.{company}")     // 设置父包名
                            .moduleName("{model}")      // 设置父包模块名
                            .entity("domain")           // 设置实体类包名
                            .service("service")         // 设置Service接口包名
                            .serviceImpl("service.impl")// 设置Service实现类包名
                            .controller("controller")   // 设置Controller包名
                            .mapper("mapper")           // 设置Mapper接口文件包名
                            .xml("mappers")             // 设置Mapper XML文件包名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + dir + "\\src\\main\\resources\\mapper"));
                })
                .strategyConfig(builder -> {
                    builder.addInclude(tables)                              // 设置需要生成的表名
                            .addTablePrefix(tablePrefix)                    // 设置表前缀

                            .serviceBuilder()                               // 设置 Service 层模板
                            .formatServiceFileName("%sService")
                            .formatServiceImplFileName("%sServiceImpl")

                            .entityBuilder()                                // 设置实体类模板
                            .enableLombok()                                 // 启用 Lombok
                            .logicDeleteColumnName("deleted")               // 逻辑删除字段名(数据库字段)
                            .enableTableFieldAnnotation()                   // 开启生成实体时生成字段注解

                            .controllerBuilder()
                            .formatFileName("%sController")
                            .enableRestStyle()                              // 启用 REST 风格

                            .mapperBuilder()                                // Mapper 策略配置
                            .enableBaseResultMap()                          // 生成通用的resultMap
                            .superClass(BaseMapper.class)                   // 设置父类
                            .formatMapperFileName("%sMapper")
                            .enableMapperAnnotation()
                            .formatXmlFileName("%sMapper");
                })
                //.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();     // 执行生成
    }
}

2、静态工具

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

因为静态方法无法读取类上的泛型,所以MP在使用静态工具读取表信息时,需要传入PO实体类的Class字节码,MP再通过反射获取到表信息。其中新增和修改的方法由于需要传入实体类对象,因此不用传入实体类的Class字节码。

代码示例:

@Test
void testDbGet() {
    User user = Db.getById(1L, User.class);
    System.out.println(user);
}

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

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

3、逻辑删除

(1)介绍

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

  • 在表中添加一个字段标记数据是否被删除,逻辑删除字段的属性通常是IntegerBoolean类型。
  • 当删除数据时把标记置为1,1表示已删除
  • 查询时只查询标记为0的数据,0表示未删除

同理更新操作也需要加上deleted = 0,所以一旦采用了逻辑删除,所有的查询、删除、更新逻辑都要跟着变化,非常麻烦。

为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们只需要在application.yaml文件中配置逻辑删除的字段名称和值即可。
 

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

代码示例:

@SpringBootTest
class IAddressServiceTest {
    @Autowired
    private IAddressService addressService;

    @Test
    void testLogicDelete() {
        // 删除
        addressService.removeById(59L);
        // 查询
        Address address = addressService.getById(59L);
        System.out.println("address = " + address);
    }
}

(2)@TableLogic

  • @TableLogic注解用于标记实体类中的逻辑删除字段。
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("address")
public class Address implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    // 省略...

    /**
     * 逻辑删除
     */
    @TableLogic // @TableLogic注解用于标记实体类中的逻辑删除字段
    //@TableLogic(value = "0", delval = "1") // value表示默认逻辑未删除值,delval表示默认逻辑删除值 (这两个值可无、会自动获取全局配置)
    @TableField("deleted")
    private Boolean deleted;
}

使用这种方式就不用在application.yaml中配置MP逻辑删除字段了,直接在逻辑删除字段属性上加该注解即可。

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

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

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

实际业务应该有回收站的逻辑处理数据

4、枚举处理器

当实体类属性是枚举类型,在与数据库的字段类型做转换时,底层默认使用的是MyBatis提供的EnumOrdinalTypeHandler枚举类型处理器。

但是这个并不好用,所以MP对类型处理器做了增强,其中增强后的枚举处理器叫MybatisEnumTypeHandler,JSON处理器叫AbstractJsonTypeHandler

(1)定义枚举,标记@EnumValue

代码示例:

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;

@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结");

    @EnumValue
    private final int value;
    private final String desc;

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

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

(2)配置枚举处理器

在application.yaml文件中配置枚举处理器

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

如果我们想向前端返回指定的枚举属性,例如value状态值desc描述,SpringMVC负责处理响应数据,它在底层处理Json时用的是jackson,所以我们只需要使用jackson提供的@JsonValue注解,来标记JSON序列化后展示的字段。

代码示例:

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;

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

效果:

5、JSON类型处理器

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

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

因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器(SpringMVC底层默认也是使用的这个类)。

(1)定义接收Json的实体类

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

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")  // 为了方便构建对象,为有参构造提供静态方法,名为of,UserInfo.of()
public class UserInfo {
    private Integer age;
    private String intro;
    private String gender;
}

(2)指定类型处理器


将User和UserVO类的info字段修改为UserInfo类型,并声明类型处理器@TableField(typeHandler = JacksonTypeHandler.class)。另外,将info改为对象类型后出现对象嵌套,在复杂嵌套查询时需要使用resultMap结果集映射,否则无法映射。所以还需要再@TableName注解中添加autoResultMap=true确保能够正常映射。

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

如果启动mapper.xml报错,在info字段后加上, typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler就解决了。

效果:

6、yaml配置加密

目前我们配置文件中的很多参数都是明文,如果开发人员发生流动,很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。

以数据库的用户名和密码为例:

(1)生成密钥        

首先,我们利用MP提供的AES工具生成一个随机秘钥,然后对用户名、密码加密。

import com.baomidou.mybatisplus.core.toolkit.AES;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class MpDemoApplicationTest {
    public static final String USERNAME = "root";
    public static final String PASSWORD = "123456";

    @Test
    void testEncrypt() {
        // 生成 16 位随机 AES 密钥
        String randomKey = AES.generateRandomKey();
        System.out.println("randomKey = " + randomKey); // randomKey = 7pSEa6F9TnYacTNJ

        // 利用密钥对用户名加密
        String username = AES.encrypt(USERNAME, randomKey);
        System.out.println("username = " + username);   // username = O4Yq+WKYGlPW5t8QvgrhUQ==

        // 利用密钥对用户名加密
        String password = AES.encrypt(PASSWORD, randomKey);
        System.out.println("password = " + password);   // password = cDYHnWysq07zUIAy1tcbRQ==
    }
}
(2)修改配置

修改application.yaml文件,把jdbc的用户名、密码修改为刚刚加密生成的密文。

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: mpw:O4Yq+WKYGlPW5t8QvgrhUQ== # 密文要以 mpw:开头
    password: mpw:cDYHnWysq07zUIAy1tcbRQ== # 密文要以 mpw:开头
(3)配置密钥运行参数

在启动项目的时候,需要把刚才生成的AES秘钥添加到Jar启动参数中,像这样:

--mpw.key=7pSEa6F9TnYacTNJ,新版本idea添加Program arguments中设置,界面如下

单元测试的时候不能添加启动参数,所以要在测试类的注解上配置:@SpringBootTest(args = "--mpw.key=7pSEa6F9TnYacTNJ")

(4)实现原理

SpringBoot提供修改Spring环境后置处理器【EnvironmentPostProcessor】,允许在应用程序之前操作环境属性值,MyBatisPlus对其进行了重写实现。

package com.baomidou.mybatisplus.autoconfigure;

import com.baomidou.mybatisplus.core.toolkit.AES;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.SimpleCommandLinePropertySource;

import java.util.HashMap;

/**
 * 安全加密处理器
 *
 * @author hubin
 * @since 2020-05-23
 */
public class SafetyEncryptProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        /**
         * 命令行中获取密钥
         */
        String mpwKey = null;
        for (PropertySource<?> ps : environment.getPropertySources()) {
            if (ps instanceof SimpleCommandLinePropertySource) {
                SimpleCommandLinePropertySource source = (SimpleCommandLinePropertySource) ps;
                mpwKey = source.getProperty("mpw.key");
                break;
            }
        }
        /**
         * 处理加密内容
         */
        if (StringUtils.isNotBlank(mpwKey)) {
            HashMap<String, Object> map = new HashMap<>();
            for (PropertySource<?> ps : environment.getPropertySources()) {
                if (ps instanceof OriginTrackedMapPropertySource) {
                    OriginTrackedMapPropertySource source = (OriginTrackedMapPropertySource) ps;
                    for (String name : source.getPropertyNames()) {
                        Object value = source.getProperty(name);
                        if (value instanceof String) {
                            String str = (String) value;
                            if (str.startsWith("mpw:")) {
                                map.put(name, AES.decrypt(str.substring(4), mpwKey));
                            }
                        }
                    }
                }
            }
            // 将解密的数据放入环境变量,并处于第一优先级上
            if (CollectionUtils.isNotEmpty(map)) {
                environment.getPropertySources().addFirst(new MapPropertySource("custom-encrypt", map));
            }
        }
    }
}

7、自动填充字段

MyBatis-Plus提供了一个便捷的自动填充功能,用于在插入或更新数据时自动填充某些字段,如创建时间、更新时间等。

(1)配置自动填充处理器

自动填充功能通过实现 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler 接口来实现。我们需要创建一个类来实现这个接口,并在其中定义插入和更新时的填充逻辑。添加@Component配置自动填充处理器类被Spring管理。

  • MyMetaObjectHandler实现MetaObjectHandler接口
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("开始插入填充...");
        // 起始版本 3.3.3(推荐)
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        // Date类型填充
        //this.strictInsertFill(metaObject, "createTime", () -> new Date(), Date.class);
        // 起始版本 3.3.0(推荐使用)
        //this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        // 也可以使用(3.3.0 该方法有bug)
        //this.fillStrategy(metaObject, "createTime", LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("开始更新填充...");
        // 起始版本 3.3.3(推荐)
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        // Date类型填充
        //this.strictUpdateFill(metaObject, "updateTime", Date::new, Date.class);
        // 起始版本 3.3.0(推荐)
        //this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        // 也可以使用(3.3.0 该方法有bug)
        //this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
    }
}

(2)添加@TableField的fill属性

在实体类中,你需要使用 @TableField 注解来标记哪些字段需要自动填充,并通过fill属性指定填充的策略。

@Data
@TableName(value = "user", autoResultMap = true)
public class User {
    // 省略...

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

注意事项:

  • 自动填充是直接给实体类的属性设置值,如果属性没有值,入库时会是null。
  • MetaObjectHandler 提供的默认方法策略是:如果属性有值则不覆盖,如果填充值为 null 则不填充。
  • 字段必须声明 @TableField 注解,并设置 fill 属性来选择填充策略。
  • 在 update(T entity, Wrapper<T> updateWrapper) 时,entity 不能为空,否则自动填充失效。
  • 在 update(Wrapper<T> updateWrapper) 时不会自动填充,需要手动赋值字段条件。
  • 使用 strictInsertFill 或 strictUpdateFill 方法可以根据注解 FieldFill.xxx、字段名和字段类型来区分填充逻辑。如果不需区分,可以使用 fillStrategy 方法。

五、插件功能

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

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

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

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

1、分页插件

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

(1)引入依赖

⚠ 注意,MyBatisPlus于 v3.5.9 起,PaginationInnerInterceptor已分离出来。如需使用,则需单独引入mybatis-plus-jsqlparser依赖!

<!-- MP分页插件 jdk 11+ 引入可选模块 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser</artifactId>
    <version>3.5.9</version>
</dependency>
<!-- MP分页插件 jdk 8+ 引入可选模块 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>

(2)配置分页内置拦截器

新建一个配置类MyBatisConfig

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;

/**
 * MyBatis配置类
 */
@Configuration
public class MybatisConfig {
    /**
     * MP拦截器
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 初始化核心插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInnerInterceptor.setMaxLimit(1000L);  // 设置单页分页条数最大限制
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
}

(3)分页API

代码示例:

@Test
void testPageQuery() {
    // 准备分页条件
    int pageNo = 1, pageSize = 2;   // 页码、每页查询条数
    // 查询条件:无
    // 分页条件
    Page<User> page = Page.of(pageNo, pageSize);
    // 排序条件
    //page.addOrder(new OrderItem("id", true)); // MP老版本排序条件写法,true为升序,false为降序
    page.addOrder(OrderItem.desc("balance"));   // 新版MP直接用OrderItem的静态方法
    page.addOrder(OrderItem.asc("id")); // 可以添加多个排序条件
    // 分页查询
    page = userService.page(page);  // 这里返回的page对象其实和上面是同一个地址,只不过是封装好分页查询结果的page对象
    // 获取page中的查询结果
    long total = page.getTotal();   // 总条数
    System.out.println("total = " + total);
    long pages = page.getPages();   // 总页数
    System.out.println("pages = " + pages);
    List<User> records = page.getRecords(); // 分页数据
    records.forEach(System.out::println);
}

效果:

2、通用分页实体

现在要实现一个用户分页查询的接口,接口规范如下:

这里需要用到4个实体:

  • PageDTO:通用分页查询条件的实体,包含分页页码、每页查询条数、排序字段、是否升序
  • UserQueryDTO:接收用户查询条件实体,为了实现分页功能去继承PageDTO
  • PageResult:分页结果实体,包含总条数、总页数、当前页数据
  • UserVO:响应用户页面视图实体,将用户VO集合封装到PageResult中返回

(1)实体类设计

分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。因此建议将分页查询条件单独定义为一个PageDTO实体

  • PageDTO 通用分页查询条件的实体
@Data
@ApiModel(description = "通用分页查询实体")
public class PageDTO {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("每页查询条数")
    private Integer pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;
}

  • UserQueryDTO 接收用户查询条件实体,继承PageDTO
@EqualsAndHashCode(callSuper = true)    // 当判断相等时先考虑父类属性再考虑子类属性,就是分页的时候把分页条件作为数据请求的的前提,然后再考虑查到了哪些匹配的数据
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQueryDTO extends PageDTO {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}
  • PageResult 分页结果实体
@Data
@ApiModel(description = "分页结果")
public class PageResult<T> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("结果集合")
    private List<T> list;
}
  • UserVO 响应用户页面视图实体,将用户VO集合封装到PageResult中返回,之前已经定义过了
@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;
}

(2)开发接口

Controller层:UserController

// in UserController
@ApiOperation("根据条件分页查询用户接口")
@GetMapping("/condition/page")
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {
    return userService.queryUserByConditionAndPage(queryDTO);
}

Service层:IUserService

// in IUserService
PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO);

// in UserServiceImpl
@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {
    // 构建分页条件
    Page<User> page = Page.of(queryDTO.getPageNo(), queryDTO.getPageSize());
    // 构建排序条件
    if (StrUtil.isNotBlank(queryDTO.getSortBy())) { // 如果排序字段不为空
        page.addOrder(new OrderItem().setColumn(queryDTO.getSortBy()).setAsc(queryDTO.getIsAsc()));
    }else { // 如果排序字段为空,默认按照更新时间排序
        page.addOrder(new OrderItem().setColumn("update_time").setAsc(false));
    }
    // 分页查询
    String name = queryDTO.getName();
    Integer status = queryDTO.getStatus();
    Page<User> p = lambdaQuery()
            .like(name != null, User::getUsername, name)
            .eq(status != null, User::getStatus, status)
            .page(page);
    // 封装VO结果
    PageResult<UserVO> result = new PageResult<>();
    result.setTotal(p.getTotal());  // 总条数
    result.setPages(p.getPages());  // 总页数
    // 当前页数据
    List<User> records = p.getRecords();
    // 其实也可以不用判断,因为如果查到的是空集合,转换完还是空集合,不影响最后的结果
    if (CollUtil.isEmpty(records)) {
        result.setList(Collections.emptyList());
        return result;
    }
    // 将用户集合拷贝为用户VO集合
    List<UserVO> userVOList = BeanUtil.copyToList(records, UserVO.class);
    result.setList(userVOList);
    return result;
}

(3)改造PageDTO实体

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

对于PageDTO构建为MP的分页对象的部分,我们完全可以在PageDTO这个实体内部中定义一个转换方法,简化开发。

一般是封装成工具类进行操作

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 PageDTO {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("每页查询条数")
    private Integer pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;

    /**
     * 在PageDTO内部将PageDTO构建为MP的分页对象,并设置排序条件
     * @param items 排序条件(可以有一个或多个)
     * @return MP的分页对象
     * @param <T> MP的分页对象的泛型
     */
    public <T> Page<T> toMpPage(OrderItem... items) {
        // 构建分页条件
        Page<T> page = Page.of(pageNo, pageSize);
        // 构建排序条件
        if (sortBy != null && sortBy.trim().length() > 0) { // 如果排序字段不为空
            page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));
        }else if (items != null) { // 如果排序字段为空,且传入的默认OrderItem不为空,按照调用者传入的默认OrderItem排序
            page.addOrder(items);
        }
        return page;
    }

    /**
     * 将PageDTO构建为MP的分页对象
     * 如果调用者不想new OrderItem对象,可以调用该方法,传入默认的排序字段和排序规则即可
     * @param defaultSortBy 排序字段
     * @param defaultAsc 排序规则,true为asc,false为desc
     * @return MP的分页对象
     * @param <T> MP的分页对象的泛型
     */
    public <T> Page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) {
        return toMpPage(new OrderItem().setColumn(defaultSortBy).setAsc(defaultAsc));
    }

    /**
     * 将PageDTO构建为MP的分页对象,排序条件按update_time更新时间降序
     * @return MP的分页对象
     * @param <T> MP的分页对象的泛型
     */
    public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
        return toMpPage("update_time", false);
    }

    /**
     * 将PageDTO构建为MP的分页对象,排序条件按create_time创建时间降序
     * @return MP的分页对象
     * @param <T> MP的分页对象的泛型
     */
    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
        return toMpPage("create_time", false);
    }
}

(4)改造PageResult实体

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

我们完全可以将 PO分页对象转换为VO分页结果对象 的逻辑,封装到 PageResult 的内部方法中,简化整个过程。

相当于定义通用业务工具类

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

    /**
     * 将PO分页对象转换为VO分页结果对象
     * @param page PO分页对象
     * @param clazz 目标VO的字节码对象
     * @return VO分页结果对象
     * @param <PO> PO实体
     * @param <VO> VO实体
     */
    public static <PO, VO> PageResult<VO> of(Page<PO> page, Class<VO> clazz) {
        // PO分页对象封装为VO结果
        PageResult<VO> result = new PageResult<>();
        result.setTotal(page.getTotal());  // 总条数
        result.setPages(page.getPages());  // 总页数
        List<PO> records = page.getRecords();    // 当前页数据
        // 其实也可以不用判断,因为如果查到的是空集合,转换完还是空集合,不影响最后的结果
        if (CollUtil.isEmpty(records)) {
            result.setList(Collections.emptyList());
            return result;
        }
        // 将PO集合拷贝为VO集合
        List<VO> userVOList = BeanUtil.copyToList(records, clazz);
        result.setList(userVOList);
        return result;
    }

    /**
     * 将PO分页对象转换为VO分页结果对象
     * @param page PO分页对象
     * @param convertor 自定义规则转换器
     * @return VO分页结果对象
     * @param <PO> PO实体
     * @param <VO> VO实体
     */
    public static <PO, VO> PageResult<VO> of(Page<PO> page, Function<PO, VO> convertor) {
        // PO分页对象封装为VO结果
        PageResult<VO> result = new PageResult<>();
        result.setTotal(page.getTotal());  // 总条数
        result.setPages(page.getPages());  // 总页数
        List<PO> records = page.getRecords();    // 当前页数据
        // 其实也可以不用判断,因为如果查到的是空集合,转换完还是空集合,不影响最后的结果
        if (CollUtil.isEmpty(records)) {
            result.setList(Collections.emptyList());
            return result;
        }
        // 将PO集合转换为VO集合,转换动作由调用者来传递
        List<VO> voList = records.stream().map(convertor).collect(Collectors.toList());
        result.setList(voList);
        return result;
    }
}

最终Service层代码:

@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {
    // 构建分页条件对象
    Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();
    // 分页查询
    String name = queryDTO.getName();
    Integer status = queryDTO.getStatus();
    Page<User> p = lambdaQuery()
            .like(name != null, User::getUsername, name)
            .eq(status != null, User::getStatus, status)
            .page(page);
    // PO分页对象封装为VO结果
    return PageResult.of(p, UserVO.class);
}

如果是希望自定义PO到VO的转换过程,可以调用重载方法of(Page<PO> page, Function<PO, VO> convertor),convertor的转换器逻辑由调用者去编写传递:

@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {
    // 构建分页条件对象
    Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();
    // 分页查询
    String name = queryDTO.getName();
    Integer status = queryDTO.getStatus();
    Page<User> p = lambdaQuery()
            .like(name != null, User::getUsername, name)
            .eq(status != null, User::getStatus, status)
            .page(page);
    // PO分页对象封装为VO结果
    return PageResult.of(p, user -> {
        // PO拷贝基础属性得到VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        // 对VO进行处理特殊逻辑
        String username = userVO.getUsername();
        // 例如用户名脱敏处理
        userVO.setUsername(username.length() > 2 ? StrUtil.fillAfter(username.substring(0, 2), '*', username.length()) : username.charAt(0) + "*");
        return userVO;
    });
}

自定义转换规则的场景,例如:

  • ① PO字段和VO字段不是包含关系,出现字段不一致。
  • ② 对VO中的属性做一些过滤、数据脱敏、加密等操作。
  • ③ 将VO中的属性继续设置数据,例如VO中的address属性,可以查询出用户所属的收获地址,设置后一并返回。

效果:


网站公告

今日签到

点亮在社区的每一天
去签到