分页查询作为企业级应用中最常见的数据交互模式之一,其核心在于平衡数据量、性能与用户体验。在前后端分离架构中,分页逻辑的合理设计直接影响接口响应速度和前端渲染效率。
典型的分页交互包含以下数据要素:
请求参数(前端→后端):
pageNumber:当前请求的页码(从1开始计数)pageSize:每页显示的数据条数sortField:排序字段名sortOrder:排序方式(asc/desc)响应数据(后端→前端):
total:符合条件的数据总量(非当前页条数)rows:当前页数据列表pageCount:总页数(可通过total/pageSize计算)实际开发中建议为pageNumber和pageSize设置默认值(如pageNumber=1, pageSize=10),避免空参数导致异常
在Spring Boot生态中,分页方案主要分为三类:
物理分页:
LIMIT 100, 20逻辑分页:
第三方组件:
在pom.xml中添加依赖时,建议使用最新稳定版本(截至2023年推荐1.4.6+):
xml复制<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
配置示例(application.yml):
yaml复制pagehelper:
helperDialect: mysql # 数据库方言
reasonable: true # 页码合理化(如pageNum<1时返回第一页)
supportMethodsArguments: true # 支持接口参数映射
PageHelper通过MyBatis拦截器机制实现分页逻辑,其核心工作流程:
PageHelper.startPage()触发拦截器关键源码片段分析:
java复制// 分页参数存储到ThreadLocal
Page<?> page = PageHelper.startPage(pageNum, pageSize);
// 后续查询会被拦截改写
List<Dept> list = deptMapper.selectAll();
// 获取分页信息
PageInfo<Dept> pageInfo = new PageInfo<>(list);
基础PageBean可扩展为通用分页响应体:
java复制@Data
public class PageResult<T> {
private Integer pageNum;
private Integer pageSize;
private Long total;
private Integer pages;
private List<T> data;
public static <T> PageResult<T> of(Page<T> page) {
PageResult<T> result = new PageResult<>();
result.setPageNum(page.getPageNum());
result.setPageSize(page.getPageSize());
result.setTotal(page.getTotal());
result.setPages(page.getPages());
result.setData(page.getResult());
return result;
}
}
改进后的Service实现应包含:
java复制@Override
public PageResult<Dept> queryDeptByPage(PageQuery query) {
// 参数校验
if (query.getPageNum() == null || query.getPageNum() < 1) {
query.setPageNum(1);
}
if (query.getPageSize() == null || query.getPageSize() > 100) {
query.setPageSize(10);
}
try {
PageHelper.startPage(query.getPageNum(), query.getPageSize());
List<Dept> depts = deptMapper.selectByCondition(query);
return PageResult.of((Page<Dept>) depts);
} finally {
// 清理ThreadLocal
PageHelper.clearPage();
}
}
对于多表关联查询,需要特别注意:
COUNT语句优化:
java复制@Select({
"SELECT * FROM dept d LEFT JOIN employee e ON d.id = e.dept_id",
"WHERE d.status = 1"
})
@Options(countStatement = "SELECT COUNT(DISTINCT d.id) FROM dept d WHERE d.status = 1")
List<Dept> selectWithEmployees();
排序参数动态处理:
java复制String orderBy = StringUtils.isNotBlank(query.getSort())
? query.getSort() + " " + query.getOrder()
: "create_time DESC";
PageHelper.startPage(pageNum, pageSize, orderBy);
当处理百万级数据时,传统LIMIT分页会出现性能瓶颈:
优化方案对比表:
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 游标分页 | WHERE id > last_id LIMIT size | 性能最优 | 不支持随机跳页 |
| 延迟关联 | 先查ID再关联 | 减少数据传输 | 需要索引支持 |
| 缓存总数 | 异步计算total | 加速分页 | 数据实时性差 |
游标分页示例:
java复制public PageResult<Dept> queryByCursor(Long lastId, Integer size) {
List<Dept> data = deptMapper.selectAfterId(lastId, size);
boolean hasNext = data.size() == size;
return new PageResult<>(data, hasNext);
}
问题1:分页结果不正确
问题2:COUNT查询慢
问题3:内存泄漏
对于特殊数据库(如达梦、人大金仓),可继承AbstractHelperDialect:
java复制public class DamengDialect extends AbstractHelperDialect {
@Override
public String getPageSql(String sql, Page page) {
StringBuilder sqlBuilder = new StringBuilder(sql);
sqlBuilder.append(" LIMIT ? OFFSET ?");
return sqlBuilder.toString();
}
}
注册方言(通过SPI机制):
与Element UI分页组件对接示例:
javascript复制// 前端请求示例
axios.get('/api/dept', {
params: {
pageNum: this.currentPage,
pageSize: this.pageSize
}
})
// 响应数据处理
this.total = response.data.total
this.tableData = response.data.rows
Ant Design Pro分页参数适配:
java复制@GetMapping("/list")
public Result list(@RequestParam Map<String, Object> params) {
// 处理Ant Design的特殊参数名
Integer current = (Integer) params.get("current");
Integer pageSize = (Integer) params.get("pageSize");
PageHelper.startPage(current, pageSize);
// ...
}
java复制public interface DeptRepository extends JpaRepository<Dept, Long> {
@Query("SELECT d FROM Dept d WHERE d.name LIKE %:name%")
Page<Dept> findByName(@Param("name") String name, Pageable pageable);
}
// 调用示例
Pageable pageable = PageRequest.of(0, 10, Sort.by("createTime").descending());
Page<Dept> page = deptRepository.findByName("技术部", pageable);
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
// 使用示例
Page<Dept> page = new Page<>(1, 10);
deptMapper.selectPage(page, Wrappers.<Dept>query().like("name", "技术部"));
通过P6Spy打印真实SQL:
properties复制# application.properties
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/test
自定义拦截器统计分页耗时:
java复制@Intercepts(@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageMonitorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
if (PageHelper.getLocalPage() != null) {
long cost = System.currentTimeMillis() - start;
if (cost > 1000) { // 超过1秒视为慢查询
log.warn("Slow page query detected: {}ms", cost);
}
}
return result;
}
}
在电商系统开发中,分页查询的优化使商品列表接口响应时间从1200ms降至200ms,关键优化点包括:
分页参数的安全防护建议:
java复制// 防止恶意超大分页
if (pageSize > 500) {
throw new BusinessException("单页数据量不能超过500条");
}
// 防止深度分页
if (pageNum > 100 && total / pageNum < 0.1) {
log.warn("Deep pagination detected: pageNum={}", pageNum);
}
对于高并发场景,建议采用分布式缓存存储热点数据的分页结果,并考虑使用布隆过滤器优化不存在数据的查询。