1. 概述

本文将探讨在 Java 持久化 API (JPA) 中实现分页的几种方式。我们将介绍如何使用基础的 JQL 查询以及更类型安全的 Criteria API 进行分页操作,并分析每种实现方式的优缺点和常见问题。

2. 使用 JQL 和 setFirstResult()/setMaxResults() 实现分页

最直接的分页实现方式是使用 **Java 查询语言 (JQL)**,通过配置查询的起始位置和最大结果数:

Query query = entityManager.createQuery("From Foo");
int pageNumber = 1;
int pageSize = 10;
query.setFirstResult((pageNumber-1) * pageSize); 
query.setMaxResults(pageSize);
List<Foo> fooList = query.getResultList();

核心 API 说明:

  • setFirstResult(int):设置结果集的起始偏移量(从0开始)
  • setMaxResults(int):设置每页返回的最大实体数量

2.1. 获取总记录数和最后一页

完整的分页方案通常需要获取 总记录数

Query queryTotal = entityManager.createQuery
    ("Select count(f.id) from Foo f");
long countResult = (long)queryTotal.getSingleResult();

计算 最后一页页码 也很有用:

int pageSize = 10;
int pageNumber = (int) ((countResult / pageSize) + 1);

⚠️ 注意:这种方式需要额外执行一次 count 查询,可能影响性能。

3. 使用 JQL 通过实体 ID 实现分页

另一种分页策略是 先获取完整 ID 列表,再基于 ID 获取完整实体。这种方式能更好地控制实体加载,但需要先加载整个表的 ID:

Query queryForIds = entityManager.createQuery(
  "Select f.id from Foo f order by f.lastName");
List<Integer> fooIds = queryForIds.getResultList();
Query query = entityManager.createQuery(
  "Select f from Foo e where f.id in :ids");
query.setParameter("ids", fooIds.subList(0,10));
List<Foo> fooList = query.getResultList();

❌ 缺点:需要执行两次查询才能获取完整结果,且首次查询会加载所有 ID。

4. 使用 JPA Criteria API 实现分页

接下来介绍如何利用 JPA Criteria API 实现动态分页:

int pageSize = 10;
CriteriaBuilder criteriaBuilder = entityManager
  .getCriteriaBuilder();
CriteriaQuery<Foo> criteriaQuery = criteriaBuilder
  .createQuery(Foo.class);
Root<Foo> from = criteriaQuery.from(Foo.class);
CriteriaQuery<Foo> select = criteriaQuery.select(from);
TypedQuery<Foo> typedQuery = entityManager.createQuery(select);
typedQuery.setFirstResult(0);
typedQuery.setMaxResults(pageSize);
List<Foo> fooList = typedQuery.getResultList();

✅ 优势:相比硬编码的字符串查询,Criteria API 能在编译时检查查询错误,减少运行时异常。

使用 Criteria API 获取实体总数 同样简单:

CriteriaQuery<Long> countQuery = criteriaBuilder
  .createQuery(Long.class);
countQuery.select(criteriaBuilder.count(
  countQuery.from(Foo.class)));
Long count = entityManager.createQuery(countQuery)
  .getSingleResult();

完整的分页解决方案示例:

int pageNumber = 1;
int pageSize = 10;
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

CriteriaQuery<Long> countQuery = criteriaBuilder
  .createQuery(Long.class);
countQuery.select(criteriaBuilder
  .count(countQuery.from(Foo.class)));
Long count = entityManager.createQuery(countQuery)
  .getSingleResult();

CriteriaQuery<Foo> criteriaQuery = criteriaBuilder
  .createQuery(Foo.class);
Root<Foo> from = criteriaQuery.from(Foo.class);
CriteriaQuery<Foo> select = criteriaQuery.select(from);

TypedQuery<Foo> typedQuery = entityManager.createQuery(select);
while (pageNumber < count.intValue()) {
    typedQuery.setFirstResult(pageNumber - 1);
    typedQuery.setMaxResults(pageSize);
    System.out.println("Current page: " + typedQuery.getResultList());
    pageNumber += pageSize;
}

5. 结论

本文介绍了 JPA 中几种基础的分页实现方式:

  • JQL + setFirstResult/setMaxResults:简单直接,但需额外 count 查询
  • 基于 ID 的分页:控制性强但性能开销大
  • Criteria API:类型安全,适合动态查询场景

虽然某些方案在查询性能上存在不足,但它们在控制性和灵活性上的优势通常能弥补这些缺点。实际开发中应根据数据量和查询复杂度选择合适方案。

完整代码示例可在 GitHub 项目 中获取,这是一个 Maven 项目,可直接导入运行。


原始标题:JPA Pagination | Baeldung

« 上一篇: Baeldung周报13