【MyBatis Plus】使用 MyBatis Plus 完成分页功能,以及通用分页实体的实现


一、MyBatis Plus 分页插件

1.1 了解 MyBatis Plus 的插件功能

当使用 MyBatis Plus 的插件功能时,可以通过配置不同的插件来满足项目的需求。这些插件可以增强 MyBatis Plus 的功能,提高数据库操作的效率和安全性。以下是对主要插件的介绍:

插件功能
PaginationInnerInterceptor自动分页
TenantLineInnerInterceptor多租户
DynamicTableNameInnerInterceptor动态表名
OptimisticLockerInnerInterceptor乐观锁
IllegalSQLInnerInterceptorSQL 性能规范
BlockAttackInnerInterceptor防止全表更新与删除
  1. PaginationInnerInterceptor(自动分页)PaginationInnerInterceptor 用于实现自动分页功能。通过配置这个插件,可以轻松地在查询操作中启用分页,而无需手动编写分页查询的 SQL。插件会自动计算起始行和结束行,以获取指定范围的数据。

  2. TenantLineInnerInterceptor(多租户)TenantLineInnerInterceptor 插件用于支持多租户架构。在多租户环境中,不同租户共享同一数据库,但数据需要分隔。这个插件可以自动在 SQL 查询中添加租户条件,以确保每个租户只能访问自己的数据。

  3. DynamicTableNameInnerInterceptor(动态表名)DynamicTableNameInnerInterceptor 允许在运行时动态更改 SQL 查询中的表名。这对于根据不同的条件选择不同的表格非常有用,例如,可以根据用户身份或其他因素选择不同的表格。

  4. OptimisticLockerInnerInterceptor(乐观锁)OptimisticLockerInnerInterceptor 用于支持乐观锁机制。乐观锁是一种并发控制方式,可以防止多个用户同时修改同一记录。这个插件会自动为实体添加乐观锁的版本字段,并在更新操作中检查版本号,以确保数据的一致性。

  5. IllegalSQLInnerInterceptor(SQL 性能规范)IllegalSQLInnerInterceptor 用于检查 SQL 查询语句的性能规范。它可以帮助开发者优化 SQL 查询,减少潜在的性能问题。通过检查 SQL 的性能规范,可以提高查询的效率。

  6. BlockAttackInnerInterceptor(防止全表更新与删除)BlockAttackInnerInterceptor 用于防止执行全表更新和删除操作。这有助于减少潜在的危险操作,以保护数据库的安全性。插件可以拦截包含特定条件的 SQL 查询,阻止执行这些操作。

通过合理配置这些插件,可以提高 MyBatis Plus 的功能和性能,以满足不同项目的需求,并且这些插件使数据库操作更加高效和安全。下面将演示如何利用 PaginationInnerInterceptor 自动分页插件来实现分页功能。

1.2 配置分页插件

在没有引入分页插件的情况下,Mybatis Plus 是不支持分页功能的,IServiceBaseMapper 中的分页方法都无法正常生效。所以,我们必须配置分页插件,简单来说,就是创建一个配置类,然后注册一个 Bean 对象:

@Configuration
public class MyBatisConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        
        // 初始化 MybatisPlusInterceptor 核心插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加自动分页插件 PaginationInnerInterceptor
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        // 返回
        return interceptor;
    }
}

对上面代码的说明:

  1. 创建一个 Java 配置类 MyBatisConfig 并使用 @Configuration 注解标记它,这将使它成为一个 Spring Boot 的配置类。

  2. 通过 @Bean 注解,创建了一个名为 mybatisPlusInterceptorBean 对象,这个 Bean 是 Mybatis Plus 核心插件 MybatisPlusInterceptor 的实例。

  3. mybatisPlusInterceptor 的 Bean 中,初始化 MybatisPlusInterceptor 的核心插件,并将分页插件 PaginationInnerInterceptor 添加到其中。

  4. 通过 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()) 语句,将分页插件添加到核心拦截器中,从而启用了分页功能。

通过这样的配置,MyBatis Plus 将能够正常支持分页功能,IServiceBaseMapper 中的分页方法将生效。当执行带有分页参数的查询时,分页插件将自动计算起始行和结束行,并修改 SQL 查询语句,以获取指定范围的数据。

1.3 测试分页功能

下面编写一个分页查询的测试类,查询第一页的数据,每一页最多两条记录:

@Test
void testPageQuery() {
    // 1. 准备分页查询条件
    Page<User> page = new Page<>();
    // 1.1.设置分页参数
    page.setCurrent(0).setSize(2);
    // 2. 分页查询,最终返回的结果保存在 page 对象中
    page = userService.page(new Page<>(0, 2));
    // 3. 总条数
    long total = page.getTotal();
    // 4. 总页数
    long pages = page.getPages();

    System.out.println("总条数:" + total + " 总页数:" + pages);
    // 5. 当前页数据
    List<User> users = page.getRecords();
    users.forEach(System.out::println);
}

对上面测试代码的解释:

  1. testPageQuery 方法中,首先准备了一个分页查询条件。这是通过创建一个 Page<User> 对象来实现的,其中 User 是要查询数据库表的实体类。

  2. 在分页查询条件中,设置了分页参数,包括当前页和每页记录数。在示例中,当前页为 0(第一页),每页最多显示 2 条记录。

  3. 接下来,使用 userService.page(new Page<>(0, 2)) 执行分页查询。page() 方法是 MyBatis Plus 提供的分页查询方法。查询结果将存储在 page 对象中。

  4. 通过 page.getTotal() 获取总记录数,这是符合查询条件的所有记录数量。

  5. 通过 page.getPages() 获取总页数,这是根据分页参数计算得出的。

  6. 最后,通过 page.getRecords() 获取当前页的数据列表,并通过 forEach 方法遍历打印每条记录。

这个测试代码用于验证分页功能是否正常工作,以及是否返回了正确的分页结果。

最终运行结果:


另外,这里用到的分页参数Page,既可以支持分页参数,也可以支持排序参数。例如按照 balance 进行降序排序:

// 1. 准备分页查询条件
Page<User> page = new Page<>();
// 1.1.设置分页参数
page.setCurrent(0).setSize(2);
// 1.2.设置排序规则
page.addOrder(new OrderItem("balance", false));

其中OrderItem类就是用于设置排序规则,第一个参数用于指定排序的字段,第二个参数则指定是否是升序排序,设置 false 则为降序。

二、实现通用分页实体

2.1 分页查询需求

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

  1. 请求方式:GET

  2. 请求路径:/users/page

  3. 请求参数:

    {
        "pageNo": 1,
        "pageSize": 5,
        "sortBy": "balance",
        "isAsc": false,
        "name": "o",
        "status": 1
    }
    
  4. 响应数据格式:

    {
        "total": 100006,
        "pages": 50003,
        "list": [
            {
                "id": 1685100878975279298,
                "username": "user_9****",
                "info": {
                    "age": 24,
                    "intro": "英文老师",
                    "gender": "female"
                },
                "status": "正常",
                "balance": 2000
            }
        ]
    }
    
  5. 特殊说明:

    • 如果排序字段为空,默认按照更新时间排序;
    • 排序字段不为空,则按照排序字段排序。

要实现上面这个接口,首先需要定义 3 个实体类:

  • UserQuery:分页查询条件的实体,包含分页、排序参数、过滤条件
  • PageDTO:分页结果实体,包含总条数、总页数、当前页数据
  • UserVO:用户页面视图实体

2.2 分页实体类

  1. UserQuery 之前已经定义过了,并且其中已经包含了过滤条件,具体代码如下:

    @Data
    @ApiModel(description = "用户查询条件实体")
    public class UserQuery {
        @ApiModelProperty("用户名关键字")
        private String name;
        @ApiModelProperty("用户状态:1-正常,2-冻结")
        private Integer status;
        @ApiModelProperty("余额最小值")
        private Integer minBalance;
        @ApiModelProperty("余额最大值")
        private Integer maxBalance;
    }
    
  2. 其中缺少的仅仅是分页条件,而分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。因此最好将分页查询条件单独定义为一个PageQuery实体。

    PageQuery是前端提交的分页查询参数,一般包含以下四个属性:

    • pageNo:页码
    • pageSize:页面大小
    • sortBy:排序字段
    • isAsc:是否升序

    下面是 PageQuery 实体类的实现代码:

    @Data
    @ApiModel(description = "分页查询实体类")
    public class PageQuery {
        @ApiModelProperty("页码")
        private Integer pageNo;
        @ApiModelProperty("页面大小")
        private Integer pageSize;
        @ApiModelProperty("排序字段")
        private String sortBy;
        @ApiModelProperty("是否升序")
        private Boolean isAsc;
    }
    
  3. 然后,让 UserQuery 继承这个实体:
    这里使用了 Lombok 提供的注解 @EqualsAndHashCode 来重写 equalshashCode 方法。

  4. 这里返回值的用户实体沿用之前实现的一个 UserVO 实体:

  5. 最后,我们需要实现PageDTO实体,用于返回分页查询的结果,由于后面可能会针对多张表进行分页查询,因此这里将其实现为泛型类,具体的实现代码如下:

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

2.3 分页查询接口

当完成了上述所有实体类的准备之后,就可以定义分页查询的接口了。

  1. 首先使用 Controller 方法,在 UserController 中新增一个 queryUsersPage 接口:

    @Api(tags = "用户管理接口")
    @RequestMapping("/user")
    @RestController
    public class UserController {
        @Autowired
        private IUserService userService;
    		
    		// ...
    		
        @GetMapping("/page")
        @ApiOperation("根据复杂条件分页查询用户接口")
        public PageDTO<UserVO> queryUsersPage(UserQuery query){
            return userService.queryUsersPage(query);
        }
    }
    
  2. 实现 Service 接口

    • 首先在 IUserService 中定义 queryUsersPage 方法
    
    public interface IUserService extends IService<User> {
    		// ...
        PageDTO<UserVO> queryUsersPage(UserQuery query);
    }
    
    
    • 然后在 UserServiceImpl 中实现 queryUsersPage 方法
    @Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        // 1. 准备分页查询条件
        // 1.1.分页条件
        Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
        // 1.2.排序条件
        if(query.getSortBy().isEmpty()){
            // 如果排序字段为空,按照更新时间排序
            page.addOrder(new OrderItem("update_time", false));
        } else {
            page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
        }
        // 2. 查询,查询的接口将封装到 page 对象中。
        this.page(page);
        // 3. 数据非空校验
        List<User> users = page.getRecords();
        if(users == null || users.isEmpty()){
            return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
        }
        // 4. 存在数据,进行实体类转换
        List<UserVO> list = BeanUtil.copyToList(users, UserVO.class);
    
        // 5. 封装返回结果
        return new PageDTO<>(page.getTotal(), page.getPages(), list);
    }
    

完成上述代码之后,发送如下请求:
返回结果:

虽然通过上面的操作之后,实现了通用分页实体,但是我们发现在实现的queryUsersPage方法中有很大篇幅的代码,像其中的准备分页条件、最终结果的转换等等基本都是通用的代码,如果要实现其他表的分页查询功能的时候,这些代码就又需要再写一遍。因此我们可以考虑将这一部分代码封装起来。

2.4 通用实体的转换

2.4.1 PageQuery 转换为 MP Page

让我们首先来改造准备分页条件的代码,在刚才的代码中,从 PageQuery 到 Mybatis Plus的 Page 之间转换的过程还是比较麻烦的。因此我们可以直接将这段代码封装到 PageQuery 实体中,作为一个工具方法来简化开发。

例如:

@ApiModel(description = "分页查询实体类")
public class PageQuery {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("页面大小")
    private Integer pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;


    /**
     * 将 PageQuery 转化为 Mybatis Plus Page
     *
     * @param orderItems 手动设置排序条件
     * @param <T>        泛型
     * @return Mybatis Plus Page
     */
    public <T> Page<T> toMpPage(OrderItem... orderItems) {
        // 1. 分页条件
        Page<T> p = Page.of(pageNo, pageSize);
        // 2. 排序提交
        if (sortBy != null) {
            p.addOrder(new OrderItem(sortBy, isAsc));
            return p;
        }

        if (orderItems != null) {
            p.addOrder(orderItems);
        }
        return p;
    }

    // 手动传入排序方式
    public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc) {
        return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
    }

    // 默认 按照 CreateTime 降序排序
    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
        return this.toMpPage("create_time", false);
    }

    // 默认 按照 UpdateTime 降序排序
    public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
        return this.toMpPage("update_time", false);
    }
}

这样我们在编写 Service 代码的时候就可以省去对从 PageQueryPage 的转换了:

// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();

2.4.2 分页结果 PO 转换 VO

在查询出分页结果后,数据的非空校验,数据的 VO 转换同样都是模板代码,编写起来很麻烦。因此我们同样可以将其封装到 PageDTO 的工具方法中,简化整个过程:

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("分页结果实体")
public class PageDTO<T> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("当前页数据集合")
    private List<T> list;


    /**
     * 返回空的分页结果
     * @param p MyBatis Plus 的分页结果
     * @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());
    }

    /**
     * 将 MyBatis Plus 的分页结果转换为 VO 的分页结果
     * @param p MyBatis Plus 的分页结果
     * @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.isEmpty()){
            // 无数据,返回空结果
            return empty(p);
        }
        // 2. 数据转换
        List<V> vos = BeanUtil.copyToList(records, voClass);
        // 3. 封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }


    /**
     * 将 Mybatis Plus分页结果转为 VO分页结果,允许用户自定义 PO到 VO的转换方式
     * @param p Mybatis Plus的分页结果
     * @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.isEmpty()){
            // 无数据,返回空结果
            return empty(p);
        }
        // 2. 数据转换
        List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
        // 3. 封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }
}

完成了上面所有的功能之后,最后 queryUsersPage 的实现方法就变成了:

@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
    // 1. 准备分页条件
    Page<User> p = query.toMpPageDefaultSortByCreateTimeDesc();
    // 2. 分页查询
    this.page(p);
    // 3. 封装返回
    return PageDTO.of(p, UserVO.class);
}

此时,三行代码就完成了,并且如果要对其他数据库表进行分页查询,也通用能够复用这些代码。

另外,如果是希望自定义 PO 到 VO 的转换过程,可以这样做:

@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
    // 1. 准备分页条件
    Page<User> p = query.toMpPageDefaultSortByCreateTimeDesc();
    // 2. 分页查询
    this.page(p);
    // 3. 封装返回
    return PageDTO.of(p, user -> {
        // 拷贝属性到 VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        // 用户名脱敏处理
        String username = userVO.getUsername();
        userVO.setUsername(username.substring(0, username.length() - 2) + "**");
        // 返回 userVO 对象
        return userVO;
    });
}

最终查询的返回结果:

发现成功对用户名实现了脱敏操作:

  • 17
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

求知.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值