滚动
滚动(Scrolling)是一种更细粒度的方法,用于迭代大型结果集块。滚动由稳定排序、滚动类型(基于偏移量或基于 Keyset 的滚动)和结果限制组成。你可以通过使用属性名称定义简单的排序表达式,并通过查询派生使用 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());
|
上面的示例展示了静态排序和限制。你可以通过定义接受 |
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 scrolling)类似于分页,使用一个偏移量计数器跳过一定数量的结果,并让数据源只返回从给定偏移量开始的结果。这种简单的机制避免了将大型结果发送到客户端应用程序。然而,大多数数据库在你的服务器返回结果之前,需要物化完整的查询结果。
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 的元素。 |
|
使用 Keyset 过滤进行滚动
基于偏移量的方法要求大多数数据库在你的服务器返回结果之前,物化整个结果。因此,虽然客户端只看到请求结果的一部分,但你的服务器需要构建完整结果,这会造成额外负载。
Keyset 过滤通过利用数据库的内置能力来处理结果子集检索,旨在减少单个查询的计算和 I/O 要求。这种方法维护一组键,通过将键传递到查询中来恢复滚动,从而有效地修改你的过滤条件。
Keyset 过滤的核心思想是使用稳定的排序顺序开始检索结果。一旦你想滚动到下一个块,你会获得一个 ScrollPosition
,用于在排序结果中重构位置。ScrollPosition
捕获当前 Window
中最后一个实体的 Keyset。为了运行查询,重构会重写条件子句,以包含所有排序字段和主键,从而使数据库能够利用潜在的索引来运行查询。数据库只需要从给定的 Keyset 位置构建一个更小的结果,而无需完全物化一个大型结果,然后再跳过结果直到达到特定偏移量。
Keyset 过滤要求 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 投影,但请确保包含所有用于排序的属性,以避免 Keyset 提取失败。
指定 Sort
顺序时,包含与查询相关的排序属性就足够了;如果你不想,无需确保查询结果是唯一的。Keyset 查询机制通过包含主键(或复合主键的任何其余部分)来修正你的排序顺序,以确保每个查询结果都是唯一的。