介绍
呈现更大的数据集可能具有挑战性。Spring Data可以通过提供各种分页功能来减轻一些痛苦。
基本
假设您要显示这样的分页组件:
首先,您需要计算元素的总数,以便可以估计页面数。
Spring Data为此提供了一个特殊的抽象:
public interface Page<T> extends Slice<T> {
// returns the number of all pages
int getTotalPages();
// returns the number of all elements
long getTotalElements();
// returns element on this page
List<T> getContent();
...
}
页面界面通常附带可寻呼,用于选择包含最多一定数量元素的特定页面:
public interface Pageable {
int getPageNumber();
int getPageSize();
...
}
两者都可以在您的@Repository界面中轻松使用:
@Entity
public class Employee {
@Id
private Long id;
private String name;
private BigDecimal salary;
}
@Repository
public interface EmployeeRepository
extends CrudRepository<Employee, Long> {
Page<Employee> findAll(Pageable pageable);
}
然后获取某个页面可以像这样完成:
private EmployeeRepository repository;
...
int page = 1;
int pageSize = 3;
Pageable pageable = PageRequest.of(page, pageSize);
Page<Employee> secondPageWithUpTo3Elements =
repository.findAll(pageable);
如果此方法已经扩展了 Jpa 存储库或分页和排序存储库,则无需将此方法显式添加到存储库接口:
public interface JpaRepository<T, ID>
extends PagingAndSortingRepository<T, ID> { ... }
public interface PagingAndSortingRepository<T, ID> {
Page<T> findAll(Pageable pageable);
...
}
带分页的动态查询
现在想象一下,您不仅需要将结果拆分为多个页面,还需要根据各种标准(例如薪资范围)添加过滤:
可分页仅存储有关页面请求的信息,因此不能将其用于传递条件,但是,它可以与其他 API 一起使用。
使用规范分页
您可以通过扩展 JPA 规范演示器接口来使用基于 JPA 标准 API 的规范:
public interface JpaSpecificationExecutor<T> {
Page<T> findAll(Specification<T> spec, Pageable pageable);
...
}
@Repository
public interface EmployeeRepository
extends JpaSpecificationExecutor<Employee> { ... }
在这里,您可以找到一个代码段,显示如何传递规范:
public static Specification<Employee> hasSalaryBetween(
BigDecimal from, BigDecimal to) {
return (Specification<Employee>) (employee, query, builder) ->
builder.between(employee.get("salary"), from, to);
}
...
PageRequest pageRequest = PageRequest.of(0, 5);
Specification<Employee> spec = hasSalaryBetween(
new BigDecimal("4000"), new BigDecimal("11000"));
Page<Employee> result = repository.findAll(spec, pageRequest);
使用查询进行分页
或者,您可以使用 Querydsl,它可以使您的代码更加类型安全,并且它还接受 Pageable 作为参数:
public interface QuerydslPredicateExecutor<T> {
Page<T> findAll(Predicate predicate, Pageable pageable);
...
}
为了获得与以前相同的结果,您可以编写如下代码:
@Repository
public interface EmployeeRepository extends
QuerydslPredicateExecutor<Employee> { ... }
...
QEmployee employee = QEmployee.employee;
Predicate predicate = employee.salary.between(
new BigDecimal("4000"), new BigDecimal("11000"));
PageRequest pageRequest = PageRequest.of(0, 5);
Page<Employee> result = repository.findAll(predicate, pageRequest);
避免分页
如果您的数据库变得足够大,则可能会遇到这样一种情况,即计数匹配元素的成本相当高(尤其是在基本分页的基础上应用动态筛选时)。而且您应该知道,计算页数需要知道总行数。简而言之,创建页面对象需要执行两个 SQL 查询。
例如,要检索第三页,需要运行以下查询:
-- fetch the third page (pageSize=5)
SELECT *
FROM employee
WHERE ... -- some criteria
OFFSET 10 ROWS
FETCH NEXT 5 ROWS ONLY;
-- count all matching elements
-- (could become your bottleneck)
SELECT COUNT(*)
FROM employee
WHERE ...; -- the same criteria as above
第一个 SELECT 应该完成得相当快,因为它期望得到少量结果。但是,假设您有一个巨大的数据集,则第二个查询可能会显著减慢当前页面的检索速度。问题在于,它需要对所有匹配的行进行计数才能获得精确的总值。
您的用户不太可能欣赏知道有1771571534元素可供浏览的价值:
切片作为替代方案
幸运的是,还有另一个名为 Slice 的有用抽象可以用于此方案:
public interface Slice<T> {
int getNumberOfElements(); // on this Slice
List<T> getContent();
boolean hasNext();
Pageable nextPageable(); // get next Slice
...
}
实际上,您可能已经注意到Page是从Slice派生的,但请注意,它们的实现可能会有很大差异。最重要的是,Slice不涉及我们刚刚讨论的昂贵的COUNT操作。
下一个切片的加载可以在单击“加载更多”按钮后调用:
...或者当用户到达页面末尾(通常称为无限滚动)::
要添加返回切片的方法,您只需编写:
@Repository
public interface EmployeeRepository extends ... {
Slice<Employee> findAll(Pageable pageable);
}
如果您想保留返回 Page s 的方法,请在此处解决方法名称冲突:
// you would like to preserve this
Page<Employee> findAll(Pageable pageable);
// ...so you need to give this method a different name
@Query("SELECT e FROM Employee e")
Slice<Employee> findAllSliced(Pageable pageable);
调用此方法的代码几乎与以前相同。唯一的区别是它返回切片而不是页面。
结束
有关一组完整的示例,请查看以下 GitHub 存储库。