MongoTemplate 如何进行分页 (Pagination) 和排序 (Sorting)?

MongoTemplate 结合 Spring Data Commons 中的 PageableSort 接口,可以非常方便地实现分页和排序功能。

以下是如何使用 MongoTemplate 进行分页和排序:

核心概念:

  1. org.springframework.data.domain.Sort:

    • 用于定义排序规则。
    • 可以通过 Sort.by(Sort.Direction direction, String... properties)Sort.by(List orders) 创建。
    • Sort.Order 对象包含排序方向 (ASCDESC) 和属性名。
  2. org.springframework.data.domain.Pageable:

    • 封装了分页信息,包括页码 (page number)、每页大小 (page size) 和排序信息 (Sort)。
    • 最常用的实现是 org.springframework.data.domain.PageRequest
  3. org.springframework.data.domain.Page:

    • 查询结果的封装,包含了当前页的数据列表、总记录数、总页数等分页信息。

使用步骤:

1. 排序 (Sorting)

你可以直接将 Sort 对象应用到 Query 中。

import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import static org.springframework.data.mongodb.core.query.Criteria.where;

// 假设有一个 User 类和 MongoTemplate 实例
// MongoTemplate mongoTemplate = ...;

// 示例1: 按单个字段升序排序
Query queryAsc = new Query(where("status").is("ACTIVE"));
queryAsc.with(Sort.by(Sort.Direction.ASC, "username")); // 按 username 升序
List<User> sortedUsersAsc = mongoTemplate.find(queryAsc, User.class);

// 示例2: 按单个字段降序排序
Query queryDesc = new Query(where("status").is("ACTIVE"));
queryDesc.with(Sort.by(Sort.Direction.DESC, "registrationDate")); // 按 registrationDate 降序
List<User> sortedUsersDesc = mongoTemplate.find(queryDesc, User.class);

// 示例3: 按多个字段排序 (先按 lastName 升序,再按 firstName 升序)
Query queryMultiSort = new Query(where("department").is("Sales"));
Sort multiSort = Sort.by(
    Sort.Order.asc("lastName"),
    Sort.Order.asc("firstName")
);
queryMultiSort.with(multiSort);
// 或者更简洁的方式:
// queryMultiSort.with(Sort.by("lastName", "firstName").ascending());
// 或者
// queryMultiSort.with(Sort.by(Sort.Direction.ASC, "lastName", "firstName"));
List<User> multiSortedUsers = mongoTemplate.find(queryMultiSort, User.class);

// 示例4: 混合排序方向
Query queryMixedSort = new Query(where("points").gt(100));
Sort mixedSort = Sort.by(
    Sort.Order.desc("points"),      // 按 points 降序
    Sort.Order.asc("username")      // 然后按 username 升序
);
queryMixedSort.with(mixedSort);
List<User> mixedSortedUsers = mongoTemplate.find(queryMixedSort, User.class);

2. 分页 (Pagination)

分页通常与排序结合使用。你需要创建一个 Pageable 对象,并将其应用到 Query 中。
MongoTemplate 本身没有直接返回 Pagefind 方法。你需要分别获取数据列表和总数,然后手动构建 Page 对象,或者使用辅助方法。

方法一:手动获取数据和总数,然后构建 PageImpl

import org.springframework.data.domain.Page;
import org.springframework.data.domain.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.Query;
import static org.springframework.data.mongodb.core.query.Criteria.where;

// MongoTemplate mongoTemplate = ...;

// 分页参数
int pageNumber = 0; // 第 0 页 (第一页)
int pageSize = 10;  // 每页 10 条记录
Sort sort = Sort.by(Sort.Direction.DESC, "registrationDate"); // 按注册日期降序

// 创建 Pageable 对象
Pageable pageable = PageRequest.of(pageNumber, pageSize, sort);

// 创建查询条件
Query query = new Query(where("status").is("ACTIVE"));

// 1. 获取总记录数 (不应用分页的 skip 和 limit, 但应用排序是可选的,通常不需要)
long totalCount = mongoTemplate.count(query, User.class); // 注意:这里的 query 不应该包含分页参数 (skip/limit)

// 2. 应用分页参数到查询中
query.with(pageable); // 这会设置 query.setSkip() 和 query.setLimit() 以及 query.setSortObject()

// 3. 获取当前页的数据列表
List<User> usersOnPage = mongoTemplate.find(query, User.class);

// 4. 构建 Page 对象
Page<User> userPage = new PageImpl<>(usersOnPage, pageable, totalCount);

// 使用 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("Content: " + userPage.getContent());

方法二:使用 MongoTemplate 的一些变体或自定义 Repository (推荐用于更简洁的代码)

虽然 MongoTemplate 核心 find 方法不直接返回 Page,但在实际项目中,你通常会:

  • 创建自定义 Repository 接口继承 PagingAndSortingRepositoryMongoRepository:
    这些接口提供了开箱即用的分页和排序方法,如 findAll(Pageable pageable)。这是最推荐的方式,因为它更简洁,也符合 Spring Data 的设计理念。

    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.mongodb.repository.MongoRepository;
    
    public interface UserRepository extends MongoRepository<User, String> {
        Page<User> findByStatus(String status, Pageable pageable);
    }
    
    // 在 Service 中使用:
    // @Autowired
    // private UserRepository userRepository;
    //
    // Pageable pageable = PageRequest.of(0, 10, Sort.by("username"));
    // Page userPage = userRepository.findByStatus("ACTIVE", pageable);
    
  • 使用 Spring Data Projections (如果只需要部分字段):
    结合 Repository 和 Pageable,可以返回投影接口或 DTO 的分页结果。

  • 通过 MongoOperations (MongoTemplate 实现的接口) 扩展方法 (不常见,但可以实现):
    可以创建自己的工具类或服务方法来封装分页逻辑。

Query 对象与 Pageable 的交互:

当你调用 query.with(pageable) 时,Spring Data MongoDB 会从 Pageable 对象中提取以下信息并设置到 Query 对象中:

  • pageable.getOffset() -> query.skip(offset)
  • pageable.getPageSize() -> query.limit(pageSize)
  • pageable.getSort() -> query.with(sort) (设置排序对象)

示例:结合查询条件、分页和排序

// MongoTemplate mongoTemplate = ...;

// 分页参数
int pageNumber = 1; // 第二页 (页码从0开始)
int pageSize = 5;
Sort sort = Sort.by(Sort.Order.asc("lastName"), Sort.Order.desc("points"));

Pageable pageable = PageRequest.of(pageNumber, pageSize, sort);

// 查询条件:部门为 "Support" 且激活状态为 true
Query query = new Query(
    new Criteria().andOperator(
        where("department").is("Support"),
        where("isActive").is(true)
    )
);

// 获取总数 (基于未分页的查询)
long totalCount = mongoTemplate.count(Query.of(query).limit(-1).skip(-1), User.class); // 克隆query并移除分页

// 应用分页到原始查询对象
query.with(pageable);

// 获取当前页数据
List<User> pagedUsers = mongoTemplate.find(query, User.class);

// 构建 Page 对象
Page<User> userPage = new PageImpl<>(pagedUsers, pageable, totalCount);

// 使用结果
System.out.println("Content for page " + userPage.getNumber() + ": " + userPage.getContent());
System.out.println("Total users matching criteria: " + userPage.getTotalElements());

关于 mongoTemplate.count() 的注意事项:

  • 传递给 mongoTemplate.count()Query 对象不应该包含 skip()limit() 设置,否则 count() 的结果会是你期望的当前页数据量(如果 limit 小于总数)而不是总匹配记录数。
  • 排序对于 count() 操作通常是不必要的,MongoDB 在计算数量时会忽略它。
  • 一个安全的做法是克隆查询对象并移除分页/排序信息,或者创建一个新的仅包含条件的查询对象用于 count
    Query countQuery = Query.of(query); // 克隆查询条件
    countQuery.limit(-1); // 确保没有 limit
    countQuery.skip(-1);  // 确保没有 skip
    countQuery.setSortObject(null); // 移除排序
    long total = mongoTemplate.count(countQuery, User.class);
    
    或者更简单地,如果你的 query 在调用 count 之前还没有被 with(pageable) 修改过,可以直接用。

总结:

  • 使用 Sort 对象定义排序规则,并用 query.with(Sort) 应用。
  • 使用 PageRequest.of(pageNumber, pageSize, sort) 创建 Pageable 对象。
  • 调用 query.with(Pageable) 将分页和排序信息应用到查询。
  • 分别执行 mongoTemplate.count() 获取总记录数和 mongoTemplate.find() 获取当前页数据。
  • 使用 PageImpl 将结果封装成 Page
  • 强烈推荐使用 Spring Data Repositories (如 MongoRepository) 来处理分页和排序,代码会更简洁且不易出错。

你可能感兴趣的:(MongoDB实战系列,MongoTemplate,Spring,Boot)