Spring Boot 整合 MongoDB:分页查询详解 (新手友好)
目录
- 引言:为什么需要分页?
- [核心概念:
Pageable
和Page
](#核心概念:Pageable 和 Page)- [
Pageable
接口:定义分页请求](#Pageable 接口:定义分页请求) - [
Page
接口:封装分页结果](#Page 接口:封装分页结果)
- [
- [方法一:使用
MongoRepository
实现分页 (推荐)](#方法一:使用 MongoRepository 实现分页 (推荐))- [步骤 1: 修改 Repository 接口](#步骤 1: 修改 Repository 接口)
- [步骤 2: 在 Service 层构建
Pageable
并调用](#步骤 2: 在 Service 层构建 Pageable 并调用) - [步骤 3: 在 Controller 层接收参数并返回
Page
对象](#步骤 3: 在 Controller 层接收参数并返回 Page 对象) - [代码示例:
MongoRepository
分页](#代码示例:MongoRepository 分页)
- [方法二:使用
MongoTemplate
实现分页 (适用于复杂查询)](#方法二:使用 MongoTemplate 实现分页 (适用于复杂查询))- [步骤 1: 在 Service 层构建
Query
和Pageable
](#步骤 1: 在 Service 层构建 Query 和 Pageable) - [步骤 2: 执行两次查询(获取数据列表和总数)](#步骤 2: 执行两次查询(获取数据列表和总数))
- [步骤 3: 手动封装
Page
对象 (PageImpl
)](#步骤 3: 手动封装 Page 对象 (PageImpl)) - [代码示例:
MongoTemplate
分页](#代码示例:MongoTemplate 分页)
- [步骤 1: 在 Service 层构建
- 分页请求参数传递 (最佳实践)
- 分页流程图 (Mermaid)
- 重点内容总结
- 结语
1. 引言:为什么需要分页?
当你的 MongoDB 集合中包含大量文档时,一次性将所有数据加载到内存中并传输给客户端是非常低效且不可行的。这会导致:
- 内存溢出 (OOM):服务器可能因加载过多数据而耗尽内存。
- 网络拥堵:大量数据传输会占用带宽,增加延迟。
- 糟糕的用户体验:用户不需要一次看到成千上万条数据,加载时间也会过长。
分页查询 (Pagination) 就是解决这个问题的标准方案。它允许你只检索和展示数据的一个小子集(一页),并提供导航到其他页面的能力。
2. 核心概念:Pageable
和 Page
Spring Data (包括 Spring Data MongoDB) 提供了一套标准的分页 API,主要围绕两个核心接口:Pageable
和 Page
。
Pageable
接口:定义分页请求
Pageable
对象封装了前端(或调用方)请求哪一页数据的信息。它主要包含:
- 页码 (Page Number) :请求第几页的数据。注意:Spring Data 的页码默认是从 0 开始的! 所以第一页是 0,第二页是 1,以此类推。
- 页面大小 (Page Size):每页希望返回多少条数据。
- 排序信息 (Sort):可选,指定按哪些字段以及按什么顺序(升序/降序)排序。
我们通常不直接实现 Pageable
接口,而是使用它的实现类 PageRequest
。
java
// 如何创建一个 Pageable 对象
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
// 请求第 1 页 (页码为 0),每页 10 条数据
Pageable pageable1 = PageRequest.of(0, 10);
// 请求第 2 页 (页码为 1),每页 5 条数据,并按 "age" 字段降序排序
Pageable pageable2 = PageRequest.of(1, 5, Sort.by(Sort.Direction.DESC, "age"));
// 请求第 1 页 (页码为 0),每页 20 条,先按 "name" 升序,再按 "createdAt" 降序
Pageable pageable3 = PageRequest.of(0, 20, Sort.by(Sort.Order.asc("name"), Sort.Order.desc("createdAt")));
注释:
PageRequest.of(page, size)
: 最常用的创建方式。PageRequest.of(page, size, Sort)
: 带排序的创建方式。Sort.by(...)
: 用于创建排序规则。
Page
接口:封装分页结果
Page<T>
对象是分页查询返回的结果。它不仅仅包含当前页的数据列表,还包含了分页相关的元数据,非常方便前端进行分页导航的展示。
Page<T>
接口常用的方法:
getContent()
: 获取当前页的数据列表 (List<T>
)。getTotalElements()
: 获取满足查询条件的总记录数 (跨所有页)。getTotalPages()
: 获取总页数。getNumber()
: 获取当前页码 (从 0 开始)。getSize()
: 获取当前页的大小 (即请求时指定的size
)。getNumberOfElements()
: 获取当前页实际包含的记录数 (可能小于size
,尤其在最后一页)。getSort()
: 获取用于查询的排序信息。isFirst()
: 是否是第一页。isLast()
: 是否是最后一页。hasNext()
: 是否有下一页。hasPrevious()
: 是否有上一页。nextPageable()
: 获取下一页的Pageable
对象 (如果存在)。previousPageable()
: 获取上一页的Pageable
对象 (如果存在)。
3. 方法一:使用 MongoRepository
实现分页 (推荐)
这是最简单、最常用 的实现分页的方式。Spring Data MongoDB 对 MongoRepository
做了强大的扩展,我们只需要在 Repository 接口的方法中添加一个 Pageable
类型的参数 ,并将**返回值类型声明为 Page<T>
**即可。Spring Data 会自动处理分页逻辑。
步骤 1: 修改 Repository 接口
在你的 UserRepository
(或任何其他 Repository) 接口中,定义一个方法,接受 Pageable
参数,返回 Page<User>
。
java
package com.example.yourproject.repository;
import com.example.yourproject.model.User;
import org.springframework.data.domain.Page; // 导入 Page
import org.springframework.data.domain.Pageable; // 导入 Pageable
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query; // 可选,用于自定义查询
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserRepository extends MongoRepository<User, String> {
// --- 标准的分页查询方法 ---
/**
* 查询所有用户,并进行分页
* Spring Data 会自动根据 Pageable 参数添加分页和排序逻辑
* @param pageable 包含页码、大小和排序信息的对象
* @return 分页结果对象 Page<User>
*/
Page<User> findAll(Pageable pageable);
// --- 带条件的分页查询 (方法名衍生) ---
/**
* 根据年龄查找用户,并进行分页
* @param age 年龄
* @param pageable 分页信息
* @return 分页结果
*/
Page<User> findByAge(int age, Pageable pageable);
/**
* 根据姓名模糊查询 (包含关键字),并进行分页
* @param nameKeyword 姓名关键字
* @param pageable 分页信息
* @return 分页结果
*/
Page<User> findByNameContainingIgnoreCase(String nameKeyword, Pageable pageable);
// --- 带条件的分页查询 (使用 @Query) ---
/**
* 使用自定义的 MongoDB JSON 查询语句进行分页查询
* ?0 代表第一个方法参数 (这里是 age), ?1 代表 Pageable (Spring Data 会处理)
* 注意:使用 @Query 时,count 查询需要单独定义或确保 Spring Data 能正确生成
* Spring Data 通常能自动生成 count 查询,但复杂查询可能需要手动提供 countQuery
* @param age 年龄下限
* @param pageable 分页信息
* @return 分页结果
*/
@Query("{ 'age' : { $gt: ?0 } }") // 查询 age 大于指定值的用户
// @Query(value = "{ 'age' : { $gt: ?0 } }", countQuery = "{ 'age' : { $gt: ?0 }, 'active': true }") // 示例:自定义 count 查询
Page<User> findUsersOlderThanWithQuery(int age, Pageable pageable);
}
重点:
- 只需将方法的最后一个参数 设置为
Pageable
类型。 - 将方法的返回值 设置为
Page<Entity>
类型 (例如Page<User>
)。 - 无论是
findAll
、方法名衍生查询 (findByAge
) 还是@Query
注解查询,都可以使用这种方式集成Pageable
。Spring Data 会自动解析Pageable
中的分页和排序信息,并应用到实际的 MongoDB 查询中。它还会自动执行一次 count 查询 来获取totalElements
。
步骤 2: 在 Service 层构建 Pageable
并调用
Service 层负责创建 Pageable
对象,通常基于 Controller 传递过来的参数(页码、大小、排序),然后调用 Repository 的分页方法。
java
package com.example.yourproject.service;
import com.example.yourproject.model.User;
import com.example.yourproject.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 分页查询所有用户
* @param pageNum 页码 (从 0 开始)
* @param pageSize 每页大小
* @param sortBy 排序字段 (可选)
* @param sortDirection 排序方向 ("asc" or "desc", 可选)
* @return 分页结果 Page<User>
*/
public Page<User> findAllUsersPaginated(int pageNum, int pageSize, String sortBy, String sortDirection) {
// 1. 构建 Sort 对象 (如果提供了排序参数)
Sort sort = Sort.unsorted(); // 默认不排序
if (sortBy != null && !sortBy.isEmpty()) {
Sort.Direction direction = Sort.Direction.ASC; // 默认升序
if ("desc".equalsIgnoreCase(sortDirection)) {
direction = Sort.Direction.DESC;
}
sort = Sort.by(direction, sortBy);
}
// 2. 构建 Pageable 对象 (使用 PageRequest)
// ** 非常重要:页码是从 0 开始的!**
Pageable pageable = PageRequest.of(pageNum, pageSize, sort);
// 3. 调用 Repository 的分页方法
Page<User> userPage = userRepository.findAll(pageable);
// 4. 返回 Page 对象,它包含了数据和分页元数据
System.out.println("Total Elements: " + userPage.getTotalElements());
System.out.println("Total Pages: " + userPage.getTotalPages());
System.out.println("Current Page Number: " + userPage.getNumber());
System.out.println("Page Size: " + userPage.getSize());
System.out.println("Users on current page: " + userPage.getContent());
return userPage;
}
/**
* 根据姓名关键字分页查询用户
*/
public Page<User> findUsersByNamePaginated(String nameKeyword, int pageNum, int pageSize) {
Pageable pageable = PageRequest.of(pageNum, pageSize, Sort.by("name").ascending()); // 按姓名升序
return userRepository.findByNameContainingIgnoreCase(nameKeyword, pageable);
}
}
注释:
- Service 方法接收页码、大小和可选的排序参数。
- 使用
Sort.by()
创建排序对象。 - 使用
PageRequest.of(...)
创建Pageable
对象,务必注意页码是从 0 开始。 - 直接调用 Repository 中定义好的、接受
Pageable
参数的方法。 - 返回的
Page<User>
对象可以直接传递给 Controller。
步骤 3: 在 Controller 层接收参数并返回 Page
对象
Controller 层负责接收来自客户端(例如 HTTP 请求)的分页参数,调用 Service 层的方法,并将返回的 Page<User>
对象序列化为 JSON(Spring Boot 自动完成)返回给客户端。
java
package com.example.yourproject.controller;
import com.example.yourproject.model.User;
import com.example.yourproject.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
/**
* 获取用户列表 (分页)
* @param page 页码 (前端通常从 1 开始,后端需要处理转换成 0-based) - 默认为 0 (第一页)
* @param size 每页大小 - 默认为 10
* @param sortBy 排序字段 - 默认为 "id"
* @param sortDir 排序方向 ("asc" or "desc") - 默认为 "asc"
* @return ResponseEntity 包装的 Page<User> 对象
*/
@GetMapping("/paginated")
public ResponseEntity<Page<User>> getAllUsersPaginated(
@RequestParam(defaultValue = "0") int page, // 使用 defaultValue 设置默认值
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir) {
// **注意**: 如果你的 API 设计希望用户传入从 1 开始的页码,
// 在调用 Service 前需要减 1 。
// int zeroBasedPage = Math.max(0, page - 1); // 转换为 0-based,并防止负数
// 这里我们假设 API 接收的就是 0-based 页码
Page<User> userPage = userService.findAllUsersPaginated(page, size, sortBy, sortDir);
// Spring Boot 会自动将 Page 对象序列化为包含 content 和分页元数据的 JSON
return ResponseEntity.ok(userPage);
}
/**
* 根据姓名搜索用户 (分页)
*/
@GetMapping("/search")
public ResponseEntity<Page<User>> searchUsersByNamePaginated(
@RequestParam String nameKeyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "5") int size) {
Page<User> userPage = userService.findUsersByNamePaginated(nameKeyword, page, size);
return ResponseEntity.ok(userPage);
}
}
注释:
- 使用
@RequestParam
接收 URL 查询参数 (?page=0&size=10&sortBy=name&sortDir=desc
)。 defaultValue
用于设置参数的默认值,提高 API 的健壮性。- 直接将 Service 返回的
Page<User>
对象放入ResponseEntity.ok()
中。Spring Boot + Jackson 会自动将其序列化成类似以下的 JSON 结构:
json
{
"content": [
// 当前页的用户对象列表...
{"id": "...", "name": "User A", "age": 30, ...},
{"id": "...", "name": "User B", "age": 25, ...}
],
"pageable": {
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 0,
"pageSize": 10,
"pageNumber": 0, // 当前页码 (0-based)
"paged": true,
"unpaged": false
},
"totalPages": 5, // 总页数
"totalElements": 48, // 总记录数
"last": false, // 是否是最后一页
"size": 10, // 页面大小
"number": 0, // 当前页码 (0-based)
"sort": { // 排序信息
"sorted": true,
"unsorted": false,
"empty": false
},
"numberOfElements": 10, // 当前页实际元素数量
"first": true, // 是否是第一页
"empty": false // 当前页是否为空
}
代码示例总结 (MongoRepository
分页)
这种方法简洁、高效,利用了 Spring Data 的强大功能,是实现分页的首选方式。
4. 方法二:使用 MongoTemplate
实现分页 (适用于复杂查询)
当你的查询逻辑非常复杂,无法通过 MongoRepository
的方法名衍生或 @Query
简单表达时,你可能需要使用 MongoTemplate
。使用 MongoTemplate
实现分页稍微复杂一些,因为你需要手动执行两次查询:
- 一次查询获取当前页的数据列表 (应用查询条件、排序、
skip
和limit
)。 - 一次查询获取满足条件的总记录数 (只应用查询条件,不应用
skip
,limit
,sort
)。
然后,你需要手动将这两部分结果组装成一个 Page<T>
对象(通常使用 PageImpl<T>
实现类)。
步骤 1: 在 Service 层构建 Query
和 Pageable
java
package com.example.yourproject.service;
import com.example.yourproject.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl; // 需要导入 PageImpl
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AdvancedUserService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 使用 MongoTemplate 进行分页查询 (示例:查找年龄大于 N 的用户)
*
* @param minAge 最小年龄
* @param pageNum 页码 (0-based)
* @param pageSize 页面大小
* @return 分页结果 Page<User>
*/
public Page<User> findUsersOlderThanWithTemplate(int minAge, int pageNum, int pageSize) {
// 1. 构建 Pageable 对象 (主要用于后续构建 PageImpl 和可能的排序)
Pageable pageable = PageRequest.of(pageNum, pageSize, Sort.by("name").ascending()); // 假设按名字升序
// 2. 构建查询条件 (Criteria)
Criteria criteria = Criteria.where("age").gt(minAge);
// 3. 构建用于获取数据列表的 Query (应用条件和分页/排序)
Query query = new Query(criteria).with(pageable); // 使用 .with(pageable) 应用分页和排序
// 4. 构建用于获取总数的 Query (只应用条件)
Query countQuery = new Query(criteria);
// 5. 执行查询获取数据列表
List<User> userList = mongoTemplate.find(query, User.class); // "users" 集合
// 6. 执行查询获取总记录数
long totalCount = mongoTemplate.count(countQuery, User.class); // "users" 集合
// 7. 手动创建 Page<T> 对象 (使用 PageImpl)
// PageImpl 需要三个参数:数据列表、Pageable 对象、总记录数
Page<User> userPage = new PageImpl<>(userList, pageable, totalCount);
System.out.println("MongoTemplate - Total Elements: " + userPage.getTotalElements());
System.out.println("MongoTemplate - Total Pages: " + userPage.getTotalPages());
System.out.println("MongoTemplate - Users on current page: " + userPage.getContent());
return userPage;
}
}
重点:
- 创建
Pageable
对象,它不仅包含分页信息,还可能包含排序信息,这些会被query.with(pageable)
使用。 - 创建
Query
对象来封装查询条件 (Criteria
)。 - 核心区别 :
query.with(pageable)
会自动将Pageable
中的分页(skip
,limit
)和排序信息添加到Query
对象中,用于获取当前页数据。 - 你需要一个单独的
countQuery
(通常只包含Criteria
,没有Pageable
) 来传递给mongoTemplate.count()
以获取总数。 - 最后,使用
new PageImpl<>(dataList, pageable, totalCount)
将查询到的数据列表、请求的Pageable
和计算出的总数封装成Page
对象。
代码示例总结 (MongoTemplate
分页)
虽然步骤稍多,但 MongoTemplate
提供了最大的灵活性来处理复杂的分页查询场景。关键在于分别获取数据列表和总数,然后使用 PageImpl
组合结果。
5. 分页请求参数传递 (最佳实践)
在设计 REST API 时,通常通过 URL 查询参数传递分页信息:
page
: 页码 (建议 API 层面接收 0-based 或 1-based,并在后端统一处理成 0-based)。size
: 每页大小。sort
: 排序字段和方向,例如:sort=name
(默认升序)sort=name,asc
(明确升序)sort=name,desc
(降序)sort=age,desc&sort=name,asc
(多字段排序:先按年龄降序,再按姓名升序)
Spring MVC 可以自动将这些参数绑定到 Controller 方法的 Pageable
参数上,但这需要额外配置或使用 Spring HATEOAS 等库。直接接收 page
, size
, sortBy
, sortDir
参数如上述 Controller 示例所示,是更常见且易于理解的做法。
6. 分页流程图 (Mermaid)
流程图说明:
- 客户端发送带有分页参数(页码、大小、排序)的 HTTP 请求。
- Controller 接收这些参数,调用 Service 层的方法。
- Service 层创建
Pageable
对象。- 如果使用
MongoRepository
: 直接调用 Repository 中定义好的、接受Pageable
参数的方法。Spring Data 会自动与 MongoDB 交互,执行数据查询和总数查询,并返回Page
对象。 - 如果使用
MongoTemplate
: Service 层需要构建两个Query
对象(一个带分页用于查数据,一个不带分页用于查总数),分别调用mongoTemplate.find()
和mongoTemplate.count()
,最后手动将结果组装成PageImpl
对象。
- 如果使用
- Service 层将得到的
Page
对象返回给 Controller。 - Controller 将
Page
对象作为 HTTP 响应体返回,Spring Boot 自动将其序列化为 JSON。 - 客户端收到 JSON 数据,其中包含当前页的数据 (
content
) 和分页元数据 (totalPages
,totalElements
,number
,size
等),可以据此渲染页面和分页控件。
7. 重点内容总结
- 分页目的: 处理大量数据,提高性能和用户体验。
- 核心接口 :
Pageable
: 定义分页请求 (页码、大小、排序),常用实现PageRequest.of(...)
。Page<T>
: 封装分页结果(当前页数据列表 + 分页元数据如总数、总页数等)。
- 页码基准 : Spring Data 页码从 0 开始计数!API 设计时要注意转换。
MongoRepository
分页 (推荐) :- 在 Repository 方法参数中添加
Pageable
。 - 将方法返回值设为
Page<T>
。 - Spring Data 自动处理查询、计数和
Page
对象构建。非常方便。
- 在 Repository 方法参数中添加
MongoTemplate
分页 (适用于复杂查询) :- 需要手动执行两次查询 :
find()
(带query.with(pageable)
) 获取数据列表,count()
(不带pageable
) 获取总数。 - 使用
new PageImpl<>(list, pageable, totalCount)
手动构建Page
对象。
- 需要手动执行两次查询 :
Page<T>
对象的重要性: 它包含了前端实现分页导航所需的所有信息。- API 参数 : 通常使用
page
,size
,sort
(或sortBy
,sortDir
) 作为 URL 查询参数。
8. 结语
掌握 Spring Boot 中 MongoDB 的分页查询是开发可扩展应用程序的关键技能。MongoRepository
提供了一种极其简洁的方式来处理常见的分页需求,而 MongoTemplate
则为复杂场景提供了必要的灵活性。理解 Pageable
和 Page
的概念,并熟悉这两种实现方式,将使你能够高效地处理大规模数据集。
下一步建议:
- 在你的项目中实际应用分页查询。
- 尝试不同的排序选项 (
Sort.by(...)
)。 - 练习在 Controller 层处理 1-based 页码到 0-based 页码的转换。
- 在前端(如果涉及)使用
Page
对象返回的元数据来构建分页控件。
希望这篇详细的笔记能帮助你彻底理解和应用 Spring Boot 中的 MongoDB 分页查询!