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 项目,可直接导入运行。