滚动

滚动是遍历更大结果集块的更精细方法。滚动由一个稳定的排序、一个滚动类型(基于偏移或键集的滚动)和结果限制组成。你可以使用属性名称定义简单的排序表达式,并通过查询派生使用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的特殊化身,表示滚动操作的开始。

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
}

使用偏移量滚动

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

示例 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

使用 Keyset 筛选进行滚动

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

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

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

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

将 `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 从最开始开始,不要应用其他筛选。

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

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

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