1. 为什么需要分页查询?
在开发Web应用时,数据列表展示是最常见的需求之一。当数据量达到几百、几千甚至更多时,如果一次性加载所有数据,会导致:
- 数据库压力剧增,查询性能下降
- 网络传输数据量过大,页面加载缓慢
- 前端渲染大量DOM节点,浏览器内存占用过高
我曾在项目中遇到过这样的场景:一个用户管理页面在数据量达到5000条时,页面加载时间超过10秒,严重影响了用户体验。这就是典型的需要分页处理的案例。
2. 环境准备与项目搭建
2.1 创建SpringBoot项目
使用IDEA或Eclipse创建一个新的SpringBoot项目,选择以下依赖:
- Spring Web
- Lombok(可选但推荐)
- Thymeleaf(用于前端展示)
2.2 添加Mybatis-Plus依赖
在pom.xml中添加Mybatis-Plus starter和数据库相关依赖:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Druid连接池(推荐) --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.16</version> </dependency>注意:版本号请根据实际情况选择最新稳定版。Druid连接池不是必须的,但生产环境强烈推荐使用。
2.3 数据库配置
在application.yml中配置数据源:
spring: datasource: url: jdbc:mysql://localhost:3306/mybatis_plus_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8 username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource3. Mybatis-Plus分页实现
3.1 配置分页插件
创建配置类MybatisPlusConfig:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这里有几个关键点需要注意:
PaginationInnerInterceptor是核心分页实现类DbType.MYSQL指定了数据库类型,Mybatis-Plus会根据不同类型生成不同的分页SQL- 在SpringBoot 2.7.x及以上版本,这是推荐的分页配置方式
3.2 创建实体类和Mapper
创建User实体类:
@Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private String email; }创建Mapper接口:
@Mapper public interface UserMapper extends BaseMapper<User> { }BaseMapper已经内置了分页查询方法selectPage,我们无需额外编写SQL。
3.3 Service层实现
创建UserService接口:
public interface UserService extends IService<User> { }实现类:
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }4. 分页查询实战
4.1 基本分页查询
在Controller中添加分页查询方法:
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/page") public IPage<User> page(@RequestParam(defaultValue = "1") int current, @RequestParam(defaultValue = "10") int size) { Page<User> page = new Page<>(current, size); return userService.page(page); } }测试访问:http://localhost:8080/user/page?current=1&size=5
4.2 带条件的分页查询
实际项目中,我们经常需要根据条件筛选数据:
@GetMapping("/pageByCondition") public IPage<User> pageByCondition(@RequestParam(defaultValue = "1") int current, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String name) { Page<User> page = new Page<>(current, size); LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); if (StringUtils.isNotBlank(name)) { queryWrapper.like(User::getName, name); } return userService.page(page, queryWrapper); }4.3 自定义SQL分页
对于复杂查询,我们可以自定义SQL:
- 在Mapper接口中添加方法:
@Mapper public interface UserMapper extends BaseMapper<User> { @Select("SELECT * FROM user WHERE age > #{age}") IPage<User> selectByAge(IPage<User> page, @Param("age") Integer age); }- 在Service中调用:
public IPage<User> pageByAge(int current, int size, int age) { Page<User> page = new Page<>(current, size); return baseMapper.selectByAge(page, age); }5. 前端分页展示
5.1 Thymeleaf实现
创建Controller方法:
@Controller public class PageController { @Autowired private UserService userService; @GetMapping("/users") public String userPage(@RequestParam(defaultValue = "1") int pageNum, Model model) { Page<User> page = new Page<>(pageNum, 5); IPage<User> userPage = userService.page(page); model.addAttribute("page", userPage); return "user-list"; } }创建user-list.html:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>用户列表</title> <style> .pagination { margin-top: 20px; } .pagination a { padding: 5px 10px; margin: 0 5px; border: 1px solid #ddd; text-decoration: none; } .pagination a.active { background: #1890ff; color: white; border-color: #1890ff; } </style> </head> <body> <table border="1" cellpadding="10"> <tr> <th>ID</th> <th>姓名</th> <th>年龄</th> <th>邮箱</th> </tr> <tr th:each="user : ${page.records}"> <td th:text="${user.id}"></td> <td th:text="${user.name}"></td> <td th:text="${user.age}"></td> <td th:text="${user.email}"></td> </tr> </table> <div class="pagination"> <a th:href="@{/users(pageNum=1)}">首页</a> <a th:href="@{/users(pageNum=${page.current}-1)}" th:if="${page.current > 1}">上一页</a> <a th:each="i : ${#numbers.sequence(1, page.pages)}" th:href="@{/users(pageNum=${i})}" th:class="${i == page.current ? 'active' : ''}" th:text="${i}"></a> <a th:href="@{/users(pageNum=${page.current}+1)}" th:if="${page.current < page.pages}">下一页</a> <a th:href="@{/users(pageNum=${page.pages})}">尾页</a> </div> <div> 当前页: <span th:text="${page.current}"></span> / 总页数: <span th:text="${page.pages}"></span> | 总记录数: <span th:text="${page.total}"></span> </div> </body> </html>5.2 前后端分离实现
对于前后端分离项目,后端只需要返回分页数据JSON:
@GetMapping("/api/users") public Result<IPage<User>> listUsers(@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize) { Page<User> page = new Page<>(pageNum, pageSize); IPage<User> userPage = userService.page(page); return Result.success(userPage); }前端(Vue示例):
<template> <div> <table> <tr v-for="user in pageData.records" :key="user.id"> <td>{{ user.id }}</td> <td>{{ user.name }}</td> <td>{{ user.age }}</td> <td>{{ user.email }}</td> </tr> </table> <div class="pagination"> <button @click="changePage(1)">首页</button> <button @click="changePage(currentPage - 1)" :disabled="currentPage === 1">上一页</button> <button v-for="page in totalPages" :key="page" @click="changePage(page)" :class="{ active: page === currentPage }"> {{ page }} </button> <button @click="changePage(currentPage + 1)" :disabled="currentPage === totalPages">下一页</button> <button @click="changePage(totalPages)">尾页</button> </div> </div> </template> <script> export default { data() { return { pageData: {}, currentPage: 1, pageSize: 10, totalPages: 0 } }, created() { this.fetchData() }, methods: { async fetchData() { const res = await this.$http.get('/api/users', { params: { pageNum: this.currentPage, pageSize: this.pageSize } }) this.pageData = res.data.data this.totalPages = this.pageData.pages }, changePage(page) { if (page < 1 || page > this.totalPages) return this.currentPage = page this.fetchData() } } } </script>6. 高级功能与优化
6.1 分页参数合理化
在实际项目中,我们需要对分页参数进行校验和合理化:
@GetMapping("/safePage") public IPage<User> safePage(@RequestParam(defaultValue = "1") int current, @RequestParam(defaultValue = "10") int size) { // 限制每页最大100条 size = Math.min(size, 100); // 确保页码不小于1 current = Math.max(current, 1); Page<User> page = new Page<>(current, size); return userService.page(page); }6.2 性能优化
- 禁用COUNT查询:当不需要总记录数时
Page<User> page = new Page<>(current, size); page.setSearchCount(false); // 禁用COUNT查询- 优化COUNT SQL:对于大表,可以重写count语句
@Select("SELECT * FROM user WHERE age > #{age}") @Options(countSql = "SELECT COUNT(1) FROM user WHERE age > #{age} USE INDEX(idx_age)") IPage<User> selectByAge(IPage<User> page, @Param("age") Integer age);6.3 多表关联分页
对于多表关联查询,Mybatis-Plus也支持:
@Select("SELECT u.*, d.name as deptName FROM user u LEFT JOIN department d ON u.dept_id = d.id") IPage<User> selectUserWithDept(IPage<User> page);7. 常见问题与解决方案
7.1 分页失效问题
现象:分页配置了但不起作用,返回了全部数据
原因:
- 分页插件未正确配置
- 在事务中调用了分页方法
解决方案:
- 确保MybatisPlusConfig被Spring扫描到
- 检查是否在事务方法中调用,如果是,确保事务方法不是private的
7.2 排序问题
需求:需要按特定字段排序
解决方案:
Page<User> page = new Page<>(current, size); page.addOrder(OrderItem.desc("create_time")); // 按创建时间降序或者使用QueryWrapper:
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.orderByDesc(User::getCreateTime);7.3 自定义分页对象
有时我们需要返回更多分页信息:
@Data public class PageResult<T> { private long current; private long size; private long total; private long pages; private List<T> records; // 其他自定义字段 private Object extraInfo; public static <T> PageResult<T> success(IPage<T> page) { PageResult<T> result = new PageResult<>(); result.setCurrent(page.getCurrent()); result.setSize(page.getSize()); result.setTotal(page.getTotal()); result.setPages(page.getPages()); result.setRecords(page.getRecords()); return result; } }在Controller中使用:
@GetMapping("/customPage") public PageResult<User> customPage(@RequestParam(defaultValue = "1") int current, @RequestParam(defaultValue = "10") int size) { Page<User> page = new Page<>(current, size); IPage<User> userPage = userService.page(page); return PageResult.success(userPage); }8. 最佳实践与经验分享
8.1 统一分页参数处理
在实际项目中,建议统一处理分页参数:
- 创建分页参数类:
@Data public class PageParam { @Min(1) private int pageNum = 1; @Min(1) @Max(100) private int pageSize = 10; public <T> Page<T> toPage() { return new Page<>(pageNum, pageSize); } }- 在Controller中使用:
@GetMapping("/page") public IPage<User> page(PageParam pageParam) { return userService.page(pageParam.toPage()); }8.2 分页缓存策略
对于变化不频繁的数据,可以考虑缓存分页结果:
@Cacheable(value = "userPage", key = "#current + '-' + #size") public IPage<User> getCachedPage(int current, int size) { return userService.page(new Page<>(current, size)); }8.3 大数据量分页优化
当数据量非常大时(如百万级以上),传统的LIMIT offset, size方式性能会很差。可以采用以下优化方案:
- 游标分页:记录上一页最后一条记录的ID
@GetMapping("/cursorPage") public List<User> cursorPage(@RequestParam(required = false) Long lastId, @RequestParam(defaultValue = "10") int size) { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); if (lastId != null) { queryWrapper.gt(User::getId, lastId); } queryWrapper.orderByAsc(User::getId) .last("LIMIT " + size); return userService.list(queryWrapper); }- 子查询优化:
@Select("SELECT * FROM user WHERE id >= (SELECT id FROM user ORDER BY id LIMIT #{offset}, 1) LIMIT #{size}") List<User> selectBySubQuery(@Param("offset") long offset, @Param("size") int size);9. 项目结构与完整代码
9.1 项目结构
src/main/java ├── com.example.demo │ ├── config │ │ └── MybatisPlusConfig.java │ ├── controller │ │ ├── UserController.java │ │ └── PageController.java │ ├── entity │ │ └── User.java │ ├── mapper │ │ └── UserMapper.java │ ├── service │ │ ├── UserService.java │ │ └── impl │ │ └── UserServiceImpl.java │ └── DemoApplication.java src/main/resources ├── static ├── templates │ └── user-list.html └── application.yml9.2 完整配置类
@Configuration @MapperScan("com.example.demo.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); paginationInnerInterceptor.setDbType(DbType.MYSQL); paginationInnerInterceptor.setMaxLimit(100L); // 单页分页条数限制 paginationInnerInterceptor.setOverflow(true); // 溢出总页数后是否进行处理 interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } @Bean public ConfigurationCustomizer configurationCustomizer() { return configuration -> configuration.setUseDeprecatedExecutor(false); } }10. 测试与验证
10.1 单元测试
编写测试类验证分页功能:
@SpringBootTest class UserServiceTest { @Autowired private UserService userService; @Test void testPageQuery() { Page<User> page = new Page<>(1, 5); IPage<User> userPage = userService.page(page); assertNotNull(userPage.getRecords()); assertEquals(5, userPage.getRecords().size()); assertTrue(userPage.getTotal() > 0); } }10.2 API测试
使用Postman或curl测试API:
curl "http://localhost:8080/user/page?current=2&size=5"预期返回结果:
{ "records": [ { "id": 6, "name": "User6", "age": 26, "email": "user6@example.com" }, ... ], "total": 50, "size": 5, "current": 2, "pages": 10 }10.3 前端页面测试
访问http://localhost:8080/users,应该能看到分页展示的用户列表和分页控件。
11. 扩展思考
11.1 与其他技术的整合
- Spring Security:在分页查询中加入权限过滤
- Redis缓存:缓存热门数据的分页结果
- Elasticsearch:对于搜索场景,使用ES的分页功能
11.2 微服务场景下的分页
在微服务架构中,分页需要考虑:
- 跨服务的数据聚合分页
- 分布式事务下的分页一致性
- 服务间分页参数的传递规范
11.3 未来演进
- 响应式分页:与WebFlux整合
- 智能分页:根据网络状况动态调整分页大小
- 无限滚动:移动端友好的分页体验
在实际项目开发中,分页查询看似简单,但要做到高性能、易用且可维护,需要考虑的细节非常多。Mybatis-Plus提供的分页功能已经覆盖了大部分常见场景,通过合理的配置和使用,可以显著提高开发效率。