分页功能是后端开发中最基础也最常遇到的需求之一。在我参与过的十几个企业级Java项目中,几乎每个涉及数据展示的模块都需要实现分页查询。传统的分页实现方式往往需要手动编写大量重复代码,而PageHelper的出现彻底改变了这一局面。
PageHelper作为MyBatis生态中最流行的分页插件,其核心价值在于:开发者只需关注业务逻辑本身,无需在SQL语句或Java代码中编写任何分页逻辑。它通过拦截器机制自动处理分页查询,支持MySQL、Oracle、PostgreSQL等主流数据库,并能返回包含丰富分页信息的PageInfo对象。
在实际项目中使用PageHelper时,我发现很多开发者虽然能够快速实现基础分页功能,但对于插件的内部机制、配置参数的含义以及不同集成方式的差异往往理解不深。本文将基于我多年的实战经验,详细剖析PageHelper的两种典型集成方式,并分享一些官方文档中没有提及的实用技巧。
第一种方式是使用基础的pagehelper依赖,这种方式需要手动配置一个Java配置类。我们先来看依赖引入:
xml复制<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.3</version>
</dependency>
这种方式的特殊之处在于需要创建一个配置类来初始化PageHelper插件。以下是配置类的典型实现:
java复制@Configuration
public class MyBatisConfig {
@Bean
public PageHelper pageHelper() {
PageHelper pageHelper = new PageHelper();
Properties properties = new Properties();
properties.setProperty("dialect", "mysql");
properties.setProperty("offsetAsPageNum", "true");
properties.setProperty("rowBoundsWithCount", "true");
pageHelper.setProperties(properties);
return pageHelper;
}
}
这里配置的三个核心参数值得深入理解:
dialect:指定数据库方言。PageHelper会根据这个参数生成不同数据库的分页SQL。例如MySQL使用LIMIT,Oracle使用ROWNUM。如果配置错误,会导致生成的分页SQL语法不正确。
offsetAsPageNum:这个参数控制是否将RowBounds中的offset参数当作pageNum使用。在大多数场景下,我们直接使用pageNum和pageSize进行分页,所以这个参数保持默认值true即可。只有在特殊场景下使用RowBounds时才需要考虑调整。
rowBoundsWithCount:这是最重要的参数之一。当设置为true时,PageHelper会先执行一个count查询获取总记录数。这个功能对于需要显示总页数的前端分页组件非常关键。但要注意,在数据量特别大时(百万级以上),count操作可能会成为性能瓶颈。
实际项目经验:在电商系统的商品列表分页中,我们曾遇到count查询超时的问题。后来通过添加合适的索引和优化查询条件解决了性能问题。这也提醒我们,虽然PageHelper简化了分页实现,但数据库本身的优化仍然不可或缺。
第二种方式是使用pagehelper-spring-boot-starter,这是专门为SpringBoot项目优化的依赖:
xml复制<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
这种方式最大的优势是可以通过application.yml或application.properties文件进行配置,完全符合SpringBoot的约定优于配置原则。以下是典型的配置示例:
yaml复制pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
这些配置参数的含义和最佳实践:
helper-dialect:同传统方式,指定数据库类型。如果不配置,PageHelper会尝试自动检测,但在多数据源场景下建议显式指定。
reasonable:分页参数合理化。当设置为true时,如果pageNum<1会查询第一页,如果pageNum>总页数会查询最后一页。这个功能能有效防止恶意传入超大页码导致的性能问题。
support-methods-arguments:支持通过Mapper接口方法的参数传递分页参数。这个特性在与MyBatis注解方式结合使用时特别有用。
params:用于从Map或对象中获取分页参数。count=countSql表示会自动进行count查询。
性能优化技巧:在数据仓库类项目中,我们曾遇到分页查询性能问题。通过分析发现,countSql在某些复杂查询场景下效率不高。后来我们通过在params中配置count=optimize,让PageHelper使用优化后的count查询方案,性能提升了约40%。
为了演示分页功能,我们需要一个简单的用户表。以下是创建表的SQL语句:
sql复制CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
建议插入至少10条测试数据,这样可以更好地观察分页效果。在实际项目中,分页功能通常在数据量超过一定阈值(如100条)后才真正发挥作用。
完整的实现需要Controller、Service、Mapper三个层次。我们先看Controller层:
java复制@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private TestService testService;
@GetMapping("/select")
public Result<?> selectUserByPage(
@RequestParam(defaultValue = "1") Integer pageNumber,
@RequestParam(defaultValue = "10") Integer pageSize) {
return ResultGenerator.genSuccessResult(
testService.selectUserByPage(pageNumber, pageSize));
}
}
这里有两个细节值得注意:
Service层实现:
java复制@Service
public class TestServiceImpl implements TestService {
@Autowired
private TbUserMapper tbUserMapper;
@Override
public PageInfo<TbUser> selectUserByPage(Integer pageNumber, Integer pageSize) {
PageHelper.startPage(pageNumber, pageSize);
List<TbUser> users = tbUserMapper.selectUser();
return new PageInfo<>(users);
}
}
关键点:
Mapper接口和XML配置:
java复制@Mapper
public interface TbUserMapper {
List<TbUser> selectUser();
}
xml复制<select id="selectUser" resultType="com.wen.dto.TbUser">
SELECT id, username, password FROM tb_user
</select>
两种依赖方式返回的结果略有不同。传统方式返回的结果包含更多导航信息:
json复制{
"pageNum": 1,
"pageSize": 5,
"total": 7,
"pages": 2,
"list": [...],
"navigatepageNums": [1, 2]
}
而Starter方式的结果更加简洁:
json复制{
"total": 7,
"list": [...],
"pageNum": 1,
"pageSize": 5
}
这种差异主要是因为不同版本对PageInfo的实现略有不同。在实际项目中,我们通常会根据前端需求对返回结果进行二次封装。
分页失效问题:
总记录数不准确:
性能问题:
多数据源配置问题:
排查经验:在微服务项目中,我们曾遇到分页插件在多数据源环境下工作不正常的问题。后来发现是因为没有为每个SqlSessionFactory单独配置PageHelper。解决方法是为每个数据源创建独立的PageHelper配置。
对于多表关联查询等复杂场景,PageHelper的默认行为可能不够理想。这时可以使用自定义count语句:
xml复制<select id="selectUserWithRole" resultMap="userRoleMap">
SELECT u.*, r.role_name
FROM tb_user u LEFT JOIN tb_role r ON u.role_id = r.id
</select>
<select id="selectUserWithRole_COUNT" resultType="long">
SELECT COUNT(1) FROM tb_user u
</select>
命名规则是在原方法名后加_COUNT。这种方式可以显著提升复杂查询的分页性能。
当数据量达到百万级时,传统的LIMIT offset, size方式性能会急剧下降。可以采用"记住上次查询的最大ID"的方式优化:
java复制public PageInfo<TbUser> selectUserByLastId(Integer lastId, Integer pageSize) {
PageHelper.startPage(1, pageSize); // 注意pageNum始终传1
List<TbUser> users = tbUserMapper.selectUserAfterId(lastId);
return new PageInfo<>(users);
}
对应的SQL:
xml复制<select id="selectUserAfterId" resultType="com.wen.dto.TbUser">
SELECT id, username, password
FROM tb_user
WHERE id > #{lastId}
ORDER BY id ASC
LIMIT #{pageSize}
</select>
这种方案在移动端分页加载场景下特别有效。
PageHelper可以与各种技术栈无缝整合。例如在MyBatis-Plus项目中:
java复制public PageInfo<User> selectUserWithWrapper(PageParam param) {
PageHelper.startPage(param.getPageNum(), param.getPageSize());
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("username", param.getKeyword());
List<User> users = userMapper.selectList(wrapper);
return new PageInfo<>(users);
}
这种组合既能利用MyBatis-Plus的条件构造器,又能享受PageHelper的分页功能。
经过实际项目验证,两种依赖方式各有优缺点:
| 特性 | 传统方式 | Starter方式 |
|---|---|---|
| 配置复杂度 | 需要Java配置类 | 只需配置文件 |
| 灵活性 | 更高,可编程配置 | 相对固定 |
| SpringBoot集成度 | 需要手动集成 | 自动配置,开箱即用 |
| 版本更新 | 版本更新较慢 | 维护更活跃 |
| 多数据源支持 | 需要手动为每个数据源配置 | 同样需要特殊处理 |
选型建议:
在实际项目中,我们团队最终统一使用了Starter方式,因为它与SpringBoot生态集成更好,减少了样板代码。但对于需要特殊分页逻辑的模块(如自定义count查询),我们会临时切换到传统方式实现特定需求。