1. 为什么需要分页查询?
在Web应用开发中,数据列表展示是最常见的需求之一。当数据量达到几百条甚至上万条时,一次性加载所有数据会导致几个严重问题:
- 性能瓶颈:数据库查询大量数据会消耗大量内存和网络带宽
- 用户体验差:浏览器渲染大量DOM元素会导致页面卡顿
- 资源浪费:传输和渲染用户根本不会查看的数据
我曾在电商项目中遇到过这样的场景:商品表有10万条记录,前端一次性请求所有数据导致接口响应时间超过15秒,最终通过分页优化将首屏加载时间降到200毫秒以内。
2. 技术选型与基础准备
2.1 SpringBoot与Mybatis-plus组合优势
选择SpringBoot+Mybatis-plus实现分页主要基于以下考虑:
- 开发效率:SpringBoot的自动配置减少了XML配置
- 功能强大:Mybatis-plus在Mybatis基础上增强了CRUD操作
- 开箱即用:Mybatis-plus自带分页插件,无需手写分页逻辑
xml复制<!-- 典型依赖配置 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
2.2 数据库准备示例
以用户表为例,基础表结构设计:
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`age` int DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 核心实现步骤详解
3.1 配置分页插件
在SpringBoot启动类或配置类中添加:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
关键参数说明:
DbType.MYSQL:指定数据库类型,支持多种主流数据库overflow属性:超出最大页数是否返回首页(默认false)
3.2 分页查询接口实现
3.2.1 Controller层
java复制@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/page")
public R<Page<User>> page(@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size) {
return R.ok(userService.page(new Page<>(current, size)));
}
}
3.2.2 Service层
java复制public interface UserService extends IService<User> {
// 继承Mybatis-plus提供的通用Service接口
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// 无需实现分页方法,直接使用父类方法
}
3.2.3 Mapper层
java复制public interface UserMapper extends BaseMapper<User> {
// 无需定义分页方法
}
3.3 自定义分页查询
当需要复杂条件查询时:
java复制// Service实现类中
public Page<User> queryByCondition(Page<User> page, UserQuery query) {
return baseMapper.selectPage(page,
new LambdaQueryWrapper<User>()
.like(StringUtils.isNotBlank(query.getName()), User::getName, query.getName())
.gt(query.getMinAge() != null, User::getAge, query.getMinAge())
.lt(query.getMaxAge() != null, User::getAge, query.getMaxAge())
);
}
4. 前端对接与参数传递
4.1 请求参数规范
推荐采用RESTful风格参数:
code复制GET /user/page?current=1&size=10
4.2 响应数据结构
典型分页响应示例:
json复制{
"code": 200,
"msg": "success",
"data": {
"records": [
{
"id": 1,
"name": "张三",
"age": 25,
"email": "zhangsan@example.com"
}
],
"total": 100,
"size": 10,
"current": 1,
"pages": 10
}
}
4.3 前端分页组件集成
以Vue+ElementUI为例:
vue复制<template>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.current"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.size"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</template>
<script>
export default {
methods: {
handleSizeChange(val) {
this.pagination.size = val
this.loadData()
},
handleCurrentChange(val) {
this.pagination.current = val
this.loadData()
}
}
}
</script>
5. 高级功能与性能优化
5.1 分页性能优化技巧
-
索引优化:确保分页查询字段有合适索引
sql复制ALTER TABLE user ADD INDEX idx_name_age (name, age); -
延迟关联(大数据量时):
java复制@Select("SELECT * FROM user WHERE id IN (SELECT id FROM user WHERE age > #{minAge} LIMIT #{offset}, #{size})") List<User> pageWithDelayJoin(@Param("minAge") Integer minAge, @Param("offset") Long offset, @Param("size") Integer size); -
游标分页(适用于无限滚动):
java复制public List<User> cursorPage(Long lastId, Integer size) { return query().gt("id", lastId) .orderByAsc("id") .last("LIMIT " + size) .list(); }
5.2 多表关联分页
java复制public Page<UserVO> pageWithRole(Page<User> page, UserQuery query) {
return baseMapper.selectPageWithRole(page,
new LambdaQueryWrapper<User>()
.like(StringUtils.isNotBlank(query.getName()), User::getName, query.getName())
);
}
// Mapper中
@Select("SELECT u.*, r.role_name FROM user u LEFT JOIN role r ON u.role_id = r.id ${ew.customSqlSegment}")
Page<UserVO> selectPageWithRole(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
5.3 分布式ID分页问题
使用雪花ID等分布式ID时,避免直接使用order by id:
java复制// 错误做法:可能导致分页数据重复
page.addOrder(OrderItem.desc("id"));
// 正确做法:结合创建时间排序
page.addOrder(OrderItem.desc("create_time"));
page.addOrder(OrderItem.desc("id"));
6. 常见问题排查
6.1 分页失效排查步骤
- 检查是否配置了分页插件
- 确认Page参数是第一个参数
- 查看SQL日志是否有LIMIT语句
- 检查Wrapper是否覆盖了Page参数
6.2 总数查询慢优化
java复制// 关闭count查询
Page<User> page = new Page<>(current, size, false);
// 自定义count语句
@Select("SELECT COUNT(1) FROM user ${ew.customSqlSegment}")
Long selectCountCustom(@Param(Constants.WRAPPER) Wrapper<User> wrapper);
6.3 内存分页问题
避免在Service层手动分页:
java复制// 错误做法:先查全部再手动分页
List<User> all = list();
List<User> pageList = all.stream()
.skip((current - 1) * size)
.limit(size)
.collect(Collectors.toList());
// 正确做法:使用数据库分页
page(new Page<>(current, size));
7. 最佳实践与经验总结
-
统一分页参数封装:
java复制public class PageParam { private Integer current = 1; private Integer size = 10; private List<OrderItem> orders; // getter/setter } -
分页最大限制:
java复制// 在拦截器中限制最大分页大小 paginationInnerInterceptor.setMaxLimit(1000L); -
特殊分页需求处理:
java复制// 导出全部数据时 if (Boolean.TRUE.equals(query.getExportAll())) { page.setSearchCount(false); page.setSize(Integer.MAX_VALUE); } -
Mybatis-plus版本兼容性:
- 3.4.x版本:使用
PaginationInterceptor - 3.5.x版本:使用
MybatisPlusInterceptor包含PaginationInnerInterceptor
- 3.4.x版本:使用
在实际项目中,分页查询看似简单但隐藏着许多细节问题。我曾在金融项目中遇到过分页导致的内存溢出问题,最终发现是因为开发人员在没有限制的情况下允许导出全部数据。建议对于任何分页接口都设置合理的最大限制,并在设计初期就考虑大数据量下的分页性能问题。
