跳到主要内容

滚动

Doubao 中英对照 Scrolling

滚动是一种对较大结果集逐块进行迭代的更精细方式。滚动由稳定排序、滚动类型(基于偏移量或基于键集的滚动)以及结果限制组成。您可以使用属性名定义简单的排序表达式,并通过查询派生使用 Top 或 First 关键词 定义静态结果限制。您可以将表达式拼接起来,把多个条件合并为一个表达式。

滚动查询会返回一个 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
}

if (users.isLast() || users.isEmpty()) {
break;
}

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

ScrollPosition 用于标识某个元素在整个查询结果中的确切位置。查询执行时将位置参数视为_排他的_,即结果将从给定位置_之后_开始。ScrollPosition#offset() 和 ScrollPosition#keyset() 是 ScrollPosition 的两种特殊形式,用于指示一次滚动操作的起点。

备注

上面的示例展示了静态排序与静态限制。您也可以定义接受 Sort 对象的查询方法,以定义更复杂的排序顺序,或按请求进行排序。类似地,提供一个 Limit 对象可以让您按请求定义动态限制,而不是应用静态限制。有关动态排序与动态限制的更多信息,请参阅 查询方法详解。

通过逐个消费 Window 实例来进行滚动,需要相当多的条件判断才能达到最优的数据库往返次数,而且很容易变成一项重复性的工作;使用 WindowIterator 可以简化这一过程。

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
}
java

使用偏移量滚动​

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

示例 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
java
  • 从无偏移量开始,以包含位于位置 0 的元素。

警告

ScrollPosition.offset() 与 ScrollPosition.offset(0L) 之间存在差异。前者表示滚动操作的起点,不指向任何特定偏移量;而后者标识结果中的第一个元素(位于位置 0)。鉴于滚动的_排他_特性,使用 ScrollPosition.offset(0) 会跳过第一个元素,并转换为偏移量 1。

使用键集过滤进行滚动​

基于偏移量的滚动要求大多数数据库在服务器能够返回结果之前,先将整个结果物化出来。因此,尽管客户端只看到所请求结果的一部分,您的服务器却需要构建完整的结果,从而造成额外的负载。

键集过滤利用数据库的内置能力来检索结果子集,旨在降低单次查询的计算与 I/O 开销。这种方式会维护一组键,通过将键传入查询来恢复滚动,从而有效地修正您的过滤条件。

键集过滤的核心思想是使用稳定的排序顺序开始检索结果。当您想滚动到下一个数据块时,获取一个 ScrollPosition,用它来重建在已排序结果中的位置。ScrollPosition 捕获当前 Window 中最后一个实体的键集。在执行查询时,重建过程会重写条件子句,将所有排序字段和主键都包含进来,以便数据库能够利用可能存在的索引来执行查询。数据库只需从给定的键集位置出发构建一个小得多的结果,而无需先将一个大结果完整物化出来,再逐条跳过结果直到到达特定的偏移量。

注意

键集过滤要求键集属性(即用于排序的属性)不可为 null。之所以有这一限制,是因为比较运算符在特定存储下对 null 值的处理方式,以及需要针对已建立索引的数据源执行查询。对可空属性进行键集过滤会导致意外的结果。

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
java
  • 从最开头开始,不应用额外的过滤。

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

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

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