滚动
滚动(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
}
// 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
}
使用偏移量进行滚动
偏移量滚动(Offset scrolling)与分页(pagination)类似,使用一个偏移计数器(Offset counter)来跳过一定数量的结果,使数据源仅返回从给定偏移量开始的结果。这种简单的机制避免了将大量结果发送到客户端应用程序。然而,大多数数据库在服务器返回结果之前需要物化完整的查询结果。
示例 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
从无偏移开始,以包含位置
0
处的元素。
ScollPosition.offset()
和 ScollPosition.offset(0L)
之间存在区别。前者表示滚动操作的开始,不指向任何特定偏移量,而后者标识结果中的第一个元素(位置为 0
)。鉴于滚动的排他性,使用 ScollPosition.offset(0)
会跳过第一个元素,并将其转换为偏移量 1
。
使用键集过滤进行滚动
基于偏移量的分页要求大多数数据库在服务器返回结果之前需要物化整个结果集。因此,尽管客户端只看到请求结果的一部分,服务器却需要构建完整的结果集,这会导致额外的负载。
键集过滤(Keyset-Filtering)方法通过利用数据库的内置功能来实现子集检索,旨在减少单个查询的计算和 I/O 需求。该方法通过将一组键传递到查询中来维持用于恢复滚动的键集,从而有效地修改过滤条件。
键集过滤(Keyset-Filtering)的核心思想是使用稳定的排序顺序开始检索结果。当你想滚动到下一个数据块时,你会获得一个 ScrollPosition
,用于在排序结果中重建位置。ScrollPosition
捕获当前 Window
中最后一个实体的键集。为了执行查询,重建过程会重写条件子句,以包含所有排序字段和主键,从而使数据库能够利用潜在的索引来运行查询。数据库只需从给定的键集位置构造一个更小的结果集,而无需完全物化一个大的结果集,然后跳过结果直到达到特定的偏移量。
Keyset-Filtering 要求用于排序的键集属性(keyset properties)必须是非空的。这一限制是由于存储层对比较运算符的 null
值处理方式以及需要针对索引源运行查询所导致的。对可空属性进行 Keyset-Filtering 会导致意外的结果。
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
从最初开始,不应用额外的过滤。
Keyset-Filtering 在数据库中存在与排序字段匹配的索引时效果最佳,因此静态排序效果良好。应用 Keyset-Filtering 的滚动查询要求查询返回排序顺序中使用的属性,并且这些属性必须在返回的实体中进行映射。
你可以使用接口和 DTO 投影,但请确保包含所有你已排序的属性,以避免键集提取失败。
在指定 Sort
排序顺序时,只需包含与查询相关的排序属性;如果你不希望确保查询结果的唯一性,则无需这样做。键集查询机制会通过包含主键(或复合主键的其余部分)来修正你的排序顺序,以确保每个查询结果都是唯一的。