滚动

滚动是一种更细粒度的迭代方式,用于处理较大的结果集块。滚动包括稳定的排序、滚动类型(基于偏移量或键集的滚动)和结果限制。您可以使用属性名称定义简单的排序表达式,并使用TopFirst 关键字 通过查询派生来定义静态结果限制。您可以连接表达式将多个条件合并到一个表达式中。

滚动查询返回一个 Window<T>,它允许获取元素的滚动位置以获取下一个 Window<T>,直到您的应用程序已使用完整个查询结果。类似于通过获取下一批结果来使用 Java Iterator<List<…>>,查询结果滚动允许您通过 Window.positionAt(…​) 访问 ScrollPosition

Window<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", ScrollPosition.offset());
do {

  for (User u : users) {
    // consume the user
  }

  // obtain the next Scroll
  users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
} while (!users.isEmpty() && users.hasNext());

ScrollPosition 标识元素在整个查询结果中的确切位置。查询执行将位置参数视为排他性,结果将从给定位置之后开始。ScrollPosition#offset()ScrollPosition#keyset() 作为 ScrollPosition 的特殊体现,表示滚动操作的开始。

以上示例显示了静态排序和限制。您可以定义其他接受 Sort 对象的查询方法来定义更复杂的排序顺序或基于每个请求的排序。以类似的方式,提供 Limit 对象允许您基于每个请求定义动态限制,而不是应用静态限制。有关动态排序和限制的更多信息,请参阅查询方法详细信息

WindowIterator 提供了一个实用程序来简化跨 Window 的滚动,从而无需检查下一个 Window 的存在并应用 ScrollPosition

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.offset());

while (users.hasNext()) {
  User u = users.next();
  // consume the user
}

使用偏移量滚动

偏移量滚动类似于分页,使用偏移量计数器跳过一定数量的结果,并让数据源仅从给定的偏移量开始返回结果。这种简单的机制避免了将大量结果发送到客户端应用程序。但是,大多数数据库需要在服务器返回结果之前物化整个查询结果。

示例 1. 在仓库查询方法中使用 OffsetScrollPosition
interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(OffsetScrollPosition.initial()); (1)
1 从没有偏移量开始,以包含位置 0 处的元素。

ScollPosition.offset()ScollPosition.offset(0L) 之间存在差异。前者表示滚动操作的开始,指向没有特定偏移量,而后者标识结果的第一个元素(位置 0)。鉴于滚动的排他性,使用 ScollPosition.offset(0) 会跳过第一个元素并转换为偏移量 1

使用键集过滤滚动

基于偏移量的滚动需要大多数数据库在服务器返回结果之前物化整个结果。因此,虽然客户端只看到请求结果的一部分,但服务器需要构建完整的结果,这会导致额外的负载。

键集过滤方法通过利用数据库的内置功能来检索结果子集,旨在减少单个查询的计算和 I/O 需求。此方法维护一组键以通过将键传递到查询中来恢复滚动,从而有效地修改您的筛选条件。

键集过滤的核心思想是从稳定的排序顺序开始检索结果。一旦您想要滚动到下一个块,您将获得一个 ScrollPosition,用于重建排序结果中的位置。ScrollPosition 捕获当前 Window 中最后一个实体的键集。要运行查询,重建会重写条件子句以包含所有排序字段和主键,以便数据库可以利用潜在的索引来运行查询。数据库只需要从给定的键集位置构建一个更小的结果,而无需完全物化一个大的结果,然后跳过结果直到到达特定的偏移量。

键集过滤要求键集属性(用于排序的属性)不可为空。由于存储特定比较运算符的 null 值处理以及需要针对索引源运行查询,因此此限制适用。在可为空的属性上进行键集过滤会导致意外结果。

在仓库查询方法中使用 KeysetScrollPosition
interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.keyset()); (1)
1 从一开始就不要应用额外的过滤。

当您的数据库包含与排序字段匹配的索引时,键集过滤效果最佳,因此静态排序效果很好。应用键集过滤的滚动查询要求查询返回排序顺序中使用的属性,并且这些属性必须映射到返回的实体中。

您可以使用接口和 DTO 投影,但请确保包含您已排序的所有属性,以避免键集提取失败。

在指定 Sort 顺序时,包括与您的查询相关的排序属性就足够了;如果您不希望查询结果唯一,则无需确保查询结果唯一。键集查询机制通过包含主键(或复合主键的任何其余部分)来修改排序顺序,以确保每个查询结果都是唯一的。