SpringBoot+Mybatis-Plus分页查询实战指南
2026/9/16 11:14:37 网站建设 项目流程

1. 为什么需要分页查询?

在开发Web应用时,数据列表展示是最常见的需求之一。当数据量达到几百、几千甚至更多时,如果一次性加载所有数据,会导致:

  1. 数据库压力剧增,查询性能下降
  2. 网络传输数据量过大,页面加载缓慢
  3. 前端渲染大量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.DruidDataSource

3. 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; } }

这里有几个关键点需要注意:

  1. PaginationInnerInterceptor是核心分页实现类
  2. DbType.MYSQL指定了数据库类型,Mybatis-Plus会根据不同类型生成不同的分页SQL
  3. 在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:

  1. 在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); }
  1. 在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 性能优化

  1. 禁用COUNT查询:当不需要总记录数时
Page<User> page = new Page<>(current, size); page.setSearchCount(false); // 禁用COUNT查询
  1. 优化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 分页失效问题

现象:分页配置了但不起作用,返回了全部数据

原因

  1. 分页插件未正确配置
  2. 在事务中调用了分页方法

解决方案

  1. 确保MybatisPlusConfig被Spring扫描到
  2. 检查是否在事务方法中调用,如果是,确保事务方法不是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 统一分页参数处理

在实际项目中,建议统一处理分页参数:

  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); } }
  1. 在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方式性能会很差。可以采用以下优化方案:

  1. 游标分页:记录上一页最后一条记录的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); }
  1. 子查询优化
@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.yml

9.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 与其他技术的整合

  1. Spring Security:在分页查询中加入权限过滤
  2. Redis缓存:缓存热门数据的分页结果
  3. Elasticsearch:对于搜索场景,使用ES的分页功能

11.2 微服务场景下的分页

在微服务架构中,分页需要考虑:

  1. 跨服务的数据聚合分页
  2. 分布式事务下的分页一致性
  3. 服务间分页参数的传递规范

11.3 未来演进

  1. 响应式分页:与WebFlux整合
  2. 智能分页:根据网络状况动态调整分页大小
  3. 无限滚动:移动端友好的分页体验

在实际项目开发中,分页查询看似简单,但要做到高性能、易用且可维护,需要考虑的细节非常多。Mybatis-Plus提供的分页功能已经覆盖了大部分常见场景,通过合理的配置和使用,可以显著提高开发效率。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询