滚动

滚动是一种更细粒度的方法,用于遍历较大的结果集块。滚动包括稳定排序、滚动类型(基于 Offset 或 Keyset 的滚动)和结果限制。您可以使用属性名称定义简单的排序表达式,并通过查询派生使用 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 标识元素在整个查询结果中的确切位置。查询执行将 position 参数视为*不包含*的,结果将从给定位置*之后*开始。ScrollPosition#offset()ScrollPosition#keyset()ScrollPosition 的特殊表现形式,指示滚动操作的开始。

上面的示例展示了静态排序和限制。您可以选择定义接受 Sort 对象的查询方法,以定义更复杂的排序顺序或基于请求的排序。类似地,提供 Limit 对象允许您基于请求定义动态限制,而不是应用静态限制。请在查询方法详情中阅读有关动态排序和限制的更多信息。

WindowIterator 提供了一个实用工具,通过消除检查下一个 Window 是否存在并应用 ScrollPosition 的需要,从而简化了跨 Window 的滚动。

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

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

使用 Offset 进行滚动

Offset 滚动类似于分页,使用一个 Offset 计数器跳过一定数量的结果,并让数据源只返回从给定 Offset 开始的结果。这种简单的机制避免了向客户端应用程序发送大量结果。然而,大多数数据库要求在您的服务器返回结果之前,先实现完整的查询结果。

示例 1. 将 OffsetScrollPosition 与 Repository 查询方法一起使用
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 从无 offset 开始,以包含位置 0 的元素。

ScrollPosition.offset()ScrollPosition.offset(0L) 之间是有区别的。前者表示滚动操作的开始,不指向任何特定的 offset,而后者标识结果的第一个元素(位于位置 0)。考虑到滚动的*独占*性,使用 ScrollPosition.offset(0) 会跳过第一个元素,并转换为 offset 1

使用 Keyset-Filtering 进行滚动

基于 Offset 的滚动要求大多数数据库在您的服务器返回结果之前,先实现整个结果。因此,虽然客户端只看到请求结果的一部分,但您的服务器需要构建完整的结果,这会造成额外的负载。

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

Keyset-Filtering 的核心思想是使用稳定的排序顺序开始检索结果。一旦您想滚动到下一个块,就会获得一个 ScrollPosition,用于重建排序结果中的位置。ScrollPosition 捕获当前 Window 中最后一个实体的 keyset。为了运行查询,重建过程会重写 criteria 子句,以包含所有排序字段和主键,从而使数据库能够利用潜在的索引来运行查询。数据库只需从给定的 keyset 位置构建一个更小的结果,而无需完全实现大量结果,然后跳过结果直到达到特定的 offset。

Keyset-Filtering 要求 keyset 属性(用于排序的那些属性)必须是非 null 的。此限制适用于特定存储的比较运算符 null 值处理,以及针对索引源运行查询的需要。在可为 null 的属性上进行 Keyset-Filtering 将导致意外结果。

KeysetScrollPosition 与 Repository 查询方法一起使用
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 从最开始处启动,并且不应用额外的过滤。

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

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

指定 Sort 顺序时,只需包含与您的查询相关的排序属性即可;如果您不想确保查询结果是唯一的,则无需这样做。keyset 查询机制会通过包含主键(或复合主键的任何剩余部分)来修改您的排序顺序,以确保每个查询结果都是唯一的。