PageHelper分页插件

PageHelper是MyBatis中的一款通用分页插件,它专门用于简化MyBatis中的分页操作,能让我们无需手动编写分页SQL,只需一行代码即可自动实现兼容多种数据库的物理分页。这篇笔记我们介绍PageHelper插件的使用和注意事项。

引入Maven依赖

PageHelper插件提供了SpringBoot的起步依赖,我们直接引入即可,注意我们这里使用的是SpringBoot 3.5.x,如果你使用的是较低版本SpringBoot可能需要修改对应pagehelper-spring-boot-starter的版本号。

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>

相关配置

在SpringBoot工程的application.properties中,我们添加以下配置。

pagehelper.banner-enabled=false
pagehelper.helper-dialect=mysql

banner-enabled配置用于关闭SpringBoot工程启动时的PageHelper框架Banner打印,官方也不知道怎么想的非要加这样一个Banner刷存在感,而且新版本2.1.1还存在Bug,实际上你将其配置为false也还是会打印Banner,非常的气人,目前临时解决方案是添加JVM参数-Dpagehelper.banner=false,PageHelper插件社区比较小维护也比较缓慢,如果你实在受不了建议转MyBatis-Plus。

helper-dialect用于配置数据库方言,不同数据库的分页SQL语句可能不同,PageHelper插件需要根据这个配置选择使用的SQL方言。不过实际上,这个方言是可以自动识别的,如果你用的是MySQL即使不加这个配置也不会出错,这里只是显式写明。但另一种情况是使用某些国产信创数据库的MySQL或PostgreSQL兼容模式时(JDBC URL与MySQL的不同),这个配置就必须添加了,否则PageHelper插件可能不认识一些非主流数据库。

使用PageHelper实现分页

下面例子代码中,listUsers()是一个分页查询用户的方法,但我们的UserMapper中并没有写分页逻辑而是只提供了一个查询全部的方法,分页是PageHelper插件实现的。

package com.gacfox.demo.service.impl;

import com.gacfox.demo.dto.RoleDTO;
import com.gacfox.demo.dto.UserDTO;
import com.gacfox.demo.mapper.UserMapper;
import com.gacfox.demo.model.User;
import com.gacfox.demo.service.UserService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    private final UserMapper userMapper;

    @Autowired
    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public PageInfo<UserDTO> listUsers(int pageNum, int pageSize) {
        PageInfo<UserDTO> result;
        try (Page<User> ignored = PageHelper.startPage(pageNum, pageSize)) {
            List<User> users = userMapper.selectAllUsers();
            PageInfo<User> userPageInfo = new PageInfo<>(users);
            List<UserDTO> dtoList = users.stream().map(this::toUserDTO).toList();
            result = new PageInfo<>(dtoList);
            result.setTotal(userPageInfo.getTotal());
            result.setPageNum(userPageInfo.getPageNum());
            result.setPageSize(userPageInfo.getPageSize());
            result.setPages(userPageInfo.getPages());
        }
        return result;
    }

    private UserDTO toUserDTO(User user) {
        UserDTO dto = new UserDTO();
        dto.setUserId(user.getUserId());
        dto.setUsername(user.getUsername());
        dto.setEmail(user.getEmail());
        if (user.getRoleList() != null) {
            dto.setRoleList(user.getRoleList().stream().map(role -> {
                RoleDTO roleDTO = new RoleDTO();
                roleDTO.setRoleId(role.getRoleId());
                roleDTO.setRolename(role.getRolename());
                return roleDTO;
            }).toList());
        }
        return dto;
    }
}

PageHelper.startPage()会在当前线程初始化一个ThreadLocal参数存储分页信息,下一条执行SQL分页查询时,PageHelper会自动对其拦截并注入LIMIT语句,因此我们下一条userMapper.selectAllUsers()操作就会自动进行分页查询了。实际上,PageHelper还有个静态方法clearPage()用于清除当前线程中的分页参数,理论上如果不清除这些参数,后续的查询可能会意外地应用相同的分页参数导致查询结果不符合预期,不过我们不用手动调用,Pageclose()方法会调用它,这也是我们用try-with-resources语句写法的原因。

PageHelper插件还提供了一个PageInfo封装类,它可以直接从原始的持久化对象中被创建,它封装了和分页相关的信息,包括当前页码、下一页页码、页码列表和分页大小,分页查询时我们通常都需要这些参数展示分页按钮,因此这些信息从PageInfo中取即可。

低性能COUNT语句问题

前面提到PageInfo类中封装了分页信息,信息中包含了总记录数,这个数据并不是PageHelper自己编出来的,实际上,在真正的查询SQL之前,PageHelper还会自动生成一个如下格式的COUNT语句来查询总页数。

select count(0) from ( 原SQL ) tmp_count

大多数简单查询使用这个自动生成的COUNT语句都没有问题,但在复杂JOIN、包含DISTINCT等情况下可能会自动生成一个性能极低或是完全错误的SQL,这是一个生产环境下十分致命的坑点。如果你遇到查询SQL试了没什么问题,但加上PageHelper后程序直接卡死、SQL怎么都查不出来的情况,大概率就是遇到这个问题了。实际上,PageHelper也支持不自动生成COUNT语句的模式,我们可以将自动COUNT关闭,手动实现一个高性能COUNT语句来代替。下面是一个例子。

package com.gacfox.demo.service.impl;

import com.gacfox.demo.dto.RoleDTO;
import com.gacfox.demo.dto.UserDTO;
import com.gacfox.demo.mapper.UserMapper;
import com.gacfox.demo.model.User;
import com.gacfox.demo.service.UserService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    private final UserMapper userMapper;

    @Autowired
    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public PageInfo<UserDTO> listUsers(int pageNum, int pageSize) {
        PageInfo<UserDTO> result;
        try (Page<User> ignored = PageHelper.startPage(pageNum, pageSize, false)) {
            List<User> users = userMapper.selectAllUsers();
            long total = userMapper.selectUserCount();
            ((Page<User>) users).setTotal(total);
            PageInfo<User> userPageInfo = new PageInfo<>(users);
            List<UserDTO> dtoList = users.stream().map(this::toUserDTO).toList();
            result = new PageInfo<>(dtoList);
            result.setTotal(userPageInfo.getTotal());
            result.setPageNum(userPageInfo.getPageNum());
            result.setPageSize(userPageInfo.getPageSize());
            result.setPages(userPageInfo.getPages());
        }
        return result;
    }

    private UserDTO toUserDTO(User user) {
        UserDTO dto = new UserDTO();
        dto.setUserId(user.getUserId());
        dto.setUsername(user.getUsername());
        dto.setEmail(user.getEmail());
        if (user.getRoleList() != null) {
            dto.setRoleList(user.getRoleList().stream().map(role -> {
                RoleDTO roleDTO = new RoleDTO();
                roleDTO.setRoleId(role.getRoleId());
                roleDTO.setRolename(role.getRolename());
                return roleDTO;
            }).toList());
        }
        return dto;
    }
}

PageHelper.startPage()方法中,除了分页参数,我们还传入了一个false,这个参数表示禁用自动生成COUNT语句,后面代码中我们手动调用了我们手写的高性能计数方法userMapper.selectUserCount()获取总记录数,这样就能规避之前的问题了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。