第一次在若依框架中使用PageHelper做分页时,我踩了不少坑。最让我头疼的是明明按照文档写了代码,分页却总是不生效,或者数据莫名其妙乱序。后来才发现,PageHelper虽然用起来简单,但有几个隐藏的坑如果不注意,分页功能就会出问题。今天我就把这些经验分享给大家,特别是刚接触若依框架的开发者。
若依框架作为国内流行的快速开发平台,集成了MyBatis和PageHelper来实现分页功能。PageHelper通过拦截SQL语句自动添加limit条件,理论上只需要在查询前调用startPage()方法就能实现分页。但在实际项目中,我发现很多开发者(包括当初的我)都会遇到三个典型问题:startPage()调用位置不对导致分页失效、排序冲突造成数据混乱,以及PageInfo总条数不准确。
这些问题看似简单,但如果不了解PageHelper的工作原理,调试起来会非常耗时。比如有一次我在排查分页失效问题时,花了整整一个下午才发现是因为在startPage()和实际查询之间多了一个无关的查询语句。接下来,我会结合具体代码示例,详细解释这三个问题的成因和解决方案。
startPage()方法必须紧挨着要分页的SQL查询语句调用,这是PageHelper使用中最容易出错的地方。很多开发者(包括我刚开始时)会犯这样的错误:
java复制// 错误的调用方式
PageHelper.startPage(pageNum, pageSize);
// 这里可能有其他业务逻辑代码
List<User> users = userMapper.selectUserList(params);
这种情况下分页很可能会失效,因为PageHelper是基于ThreadLocal实现的,它会在startPage()方法调用时设置分页参数,然后在第一个执行的查询语句中应用这些参数。如果在startPage()和实际查询之间还有其他查询语句,分页参数就会被提前消耗掉。
正确的做法应该是:
java复制// 正确的调用方式
List<User> users;
// 业务逻辑代码...
// 在真正需要分页的查询前立即调用startPage
PageHelper.startPage(pageNum, pageSize);
users = userMapper.selectUserList(params);
另一个常见问题是在startPage()后执行了多个查询。比如:
java复制PageHelper.startPage(pageNum, pageSize);
// 第一个查询(不需要分页)
List<Department> depts = departmentMapper.selectAll();
// 第二个查询(需要分页)
List<User> users = userMapper.selectUserList(params);
这种情况下,分页参数会被第一个查询消耗,导致第二个查询没有分页效果。我建议在若依框架中,将分页查询单独封装成一个方法,确保startPage()后只有一个查询:
java复制public PageInfo<User> getPagedUsers(UserParams params) {
PageHelper.startPage(params.getPageNum(), params.getPageSize());
List<User> users = userMapper.selectUserList(params);
return new PageInfo<>(users);
}
在某些复杂业务场景下,可能需要在一个方法中执行多个分页查询。这时记得在每次分页查询后清除分页参数:
java复制// 第一个分页查询
PageHelper.startPage(pageNum1, pageSize1);
List<User> users = userMapper.selectUserList(params1);
PageHelper.clearPage();
// 第二个分页查询
PageHelper.startPage(pageNum2, pageSize2);
List<Department> depts = departmentMapper.selectDepartmentList(params2);
PageHelper.clearPage();
PageHelper的startPage()方法实际上会为查询添加默认的排序条件。如果你在SQL中也写了ORDER BY子句,就会出现两个排序条件冲突的情况,导致数据顺序混乱。这个问题在若依框架中尤其隐蔽,因为框架本身可能会自动添加一些排序逻辑。
我曾经遇到过这样的场景:前端传了排序参数,SQL中也写了ORDER BY,结果数据顺序完全不对。后来发现是PageHelper的默认排序在作怪。
解决这个问题有两种方式。第一种是修改PageHelper配置,完全禁用默认排序:
yaml复制# application.yml
pagehelper:
support-methods-arguments: true
helper-dialect: mysql
reasonable: true
params: count=countSql
# 关闭默认排序
order-by-only: false
第二种方式是在startPage()时显式指定排序字段,避免与SQL中的ORDER BY冲突:
java复制PageHelper.startPage(pageNum, pageSize, "id DESC");
若依框架的TableSupport类提供了构建分页请求的方法,它会从前端请求中提取排序参数。在使用时要注意:
java复制// 构建分页请求
PageDomain pageDomain = TableSupport.buildPageRequest();
// 如果有排序参数,使用它
if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) {
PageHelper.startPage(pageDomain.getPageNum(),
pageDomain.getPageSize(),
pageDomain.getOrderBy());
} else {
PageHelper.startPage(pageDomain.getPageNum(),
pageDomain.getPageSize());
}
在使用PageInfo封装分页结果时,经常会遇到总条数(total)与列表数据(list)数量相同的情况。这通常发生在对查询结果进行二次处理时,比如:
java复制PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectUserList(params);
PageInfo<User> pageInfo = new PageInfo<>(users);
// 对结果进行转换
List<UserDTO> userDTOs = convertToDTOs(users);
pageInfo.setList(userDTOs); // 这里会导致总条数不正确
这是因为PageHelper的分页信息是存储在第一次查询返回的List中的(通过Page对象)。当我们创建一个新的List并设置到PageInfo中时,这些隐藏的分页信息就丢失了。
解决这个问题有两种方法。第一种是保持原始列表不变,只转换需要展示的数据:
java复制PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectUserList(params);
PageInfo<User> pageInfo = new PageInfo<>(users);
// 前端只使用转换后的DTO列表
List<UserDTO> userDTOs = convertToDTOs(pageInfo.getList());
第二种方法是使用PageInfo的构造函数直接传入转换后的列表,同时保留总条数:
java复制PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectUserList(params);
// 先创建PageInfo获取分页信息
PageInfo<User> tempPageInfo = new PageInfo<>(users);
// 转换列表
List<UserDTO> userDTOs = convertToDTOs(users);
// 创建新的PageInfo并设置总条数
PageInfo<UserDTO> pageInfo = new PageInfo<>(userDTOs);
pageInfo.setTotal(tempPageInfo.getTotal());
若依框架通常使用TableDataInfo来返回分页数据。正确的做法应该是:
java复制@PostMapping("/list")
public TableDataInfo list(@RequestBody UserQuery query) {
startPage(); // 若依提供的快捷方法
List<User> users = userService.selectUserList(query);
// 先获取分页信息
PageInfo<User> pageInfo = new PageInfo<>(users);
// 转换DTO
List<UserDTO> dtos = convertToDTOs(users);
TableDataInfo dataInfo = new TableDataInfo();
dataInfo.setRows(dtos);
dataInfo.setTotal(pageInfo.getTotal());
return dataInfo;
}
在实际项目中,我们应该对分页参数进行校验,避免恶意传入过大的pageSize导致性能问题:
java复制public void startPage() {
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
if (pageNum == null || pageSize == null) {
throw new RuntimeException("分页参数不能为空");
}
if (pageSize > 100) {
pageSize = 100; // 限制最大页大小
}
PageHelper.startPage(pageNum, pageSize);
}
对于复杂的关联查询,PageHelper的分页可能会产生性能问题。这时可以考虑使用子查询先获取ID,再分页查询详细数据:
java复制PageHelper.startPage(pageNum, pageSize);
// 先分页查询ID
List<Long> ids = userMapper.selectUserIds(params);
// 再根据ID查询详细数据
List<User> users = userMapper.selectUserDetailsByIds(ids);
若依框架的分页功能是通过AOP实现的。@DataScope注解会结合PageHelper自动处理分页逻辑。了解这一点有助于在需要自定义分页逻辑时做出正确决策。框架的startPage()方法实际上是对PageHelper.startPage()的封装,添加了一些若依特有的处理逻辑。