Skip to content

Commit 0c0c1af

Browse files
Add WindowIterator and rename Scroll to Window.
The intend of WindowIterator is to support users who need to iterate multiple windows. It keeps track of the position and loads the next window if needed so that the user does not have to interact with the position at all. Also remove the Window methods to get the first/last position and enforce the index based variant. Update the documentation to make use of the newly introduced API. See: #2151 Original Pull Request: #2787
1 parent 035965a commit 0c0c1af

File tree

12 files changed

+357
-121
lines changed

12 files changed

+357
-121
lines changed

src/main/asciidoc/repositories-scrolling.adoc

+46-27
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,36 @@ Scrolling consists of a stable sort, a scroll type (Offset- or Keyset-based scro
66
You can define simple sorting expressions by using property names and define static result limiting using the <<repositories.limit-query-result,`Top` or `First` keyword>> through query derivation.
77
You can concatenate expressions to collect multiple criteria into one expression.
88

9-
Scroll queries return a `Scroll<T>` that allows obtaining the scroll position to resume to obtain the next `Scroll<T>` until your application has consumed the entire query result.
10-
Similar to consuming a Java `Iterator<List<…>>` by obtaining the next batch of results, query result scrolling lets you access the next `ScrollPosition` through `Scroll.lastScrollPosition()`.
9+
Scroll queries return a `Window<T>` that allows obtaining the scroll position to resume to obtain the next `Window<T>` until your application has consumed the entire query result.
10+
Similar to consuming a Java `Iterator<List<…>>` by obtaining the next batch of results, query result scrolling lets you access the a `ScrollPosition` through `Window.positionAt(...)`.
11+
12+
[source,java]
13+
----
14+
Window<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", OffsetScrollPosition.initial());
15+
do {
16+
17+
for (User u : users) {
18+
// consume the user
19+
}
20+
21+
// obtain the next Scroll
22+
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
23+
} while (!users.isEmpty() && !users.isLast());
24+
----
25+
26+
`WindowIterator` provides a specialized implementation that simplifies consumption of multiple `Window` instances by removing the need to check for the presence of a next `Window` and applying the `ScrollPosition`.
27+
28+
[source,java]
29+
----
30+
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
31+
.startingAt(OffsetScrollPosition.initial());
32+
33+
while (users.hasNext()) {
34+
users.next().forEach(user -> {
35+
// consume the user
36+
});
37+
}
38+
----
1139

1240
[[repositories.scrolling.offset]]
1341
===== Scrolling using Offset
@@ -22,21 +50,13 @@ However, most databases require materializing the full query result before your
2250
----
2351
interface UserRepository extends Repository<User, Long> {
2452
25-
Scroll<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
53+
Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
2654
}
2755
28-
Scroll<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", OffsetScrollPosition.initial());
29-
30-
do {
31-
32-
for (User u : users) {
33-
// consume the user
34-
}
35-
36-
// obtain the next Scroll
37-
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.lastScrollPosition());
38-
} while (!users.isEmpty() && !users.isLast());
56+
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
57+
.startingAt(OffsetScrollPosition.initial()); <1>
3958
----
59+
<1> Start from the initial offset at position 0;
4060
====
4161

4262
[[repositories.scrolling.keyset]]
@@ -50,31 +70,30 @@ This approach maintains a set of keys to resume scrolling by passing keys into t
5070

5171
The core idea of Keyset-Filtering is to start retrieving results using a stable sorting order.
5272
Once you want to scroll to the next chunk, you obtain a `ScrollPosition` that is used to reconstruct the position within the sorted result.
53-
The `ScrollPosition` captures the keyset of the last entity within the current `Scroll`.
73+
The `ScrollPosition` captures the keyset of the last entity within the current `Window`.
5474
To run the query, reconstruction rewrites the criteria clause to include all sort fields and the primary key so that the database can leverage potential indexes to run the query.
5575
The database needs only constructing a much smaller result from the given keyset position without the need to fully materialize a large result and then skipping results until reaching a particular offset.
5676

77+
[WARNING]
78+
====
79+
Keyset-Filtering requires properties used for sorting to be non nullable.
80+
This limitation applies due to the store specific `null` value handling of comparison operators as well as the need to run queries against an indexed source.
81+
Keyset-Filtering on nullable properties will lead to unexpected results.
82+
====
83+
5784
.Using `KeysetScrollPosition` with Repository Query Methods
5885
====
5986
[source,java]
6087
----
6188
interface UserRepository extends Repository<User, Long> {
6289
63-
Scroll<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
90+
Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
6491
}
6592
66-
Scroll<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", KeysetScrollPosition.initial());
67-
68-
do {
69-
70-
for (User u : users) {
71-
// consume the user
72-
}
73-
74-
// obtain the next Scroll
75-
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.lastScrollPosition());
76-
} while (!users.isEmpty() && !users.isLast());
93+
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
94+
.startingAt(KeysetScrollPosition.initial()); <1>
7795
----
96+
<1> Start at the very beginning and do not apply additional filtering.
7897
====
7998

8099
Keyset-Filtering works best when your database contains an index that matches the sort fields, hence a static sort works well.

src/main/java/org/springframework/data/domain/Scroll.java renamed to src/main/java/org/springframework/data/domain/Window.java

+23-55
Original file line numberDiff line numberDiff line change
@@ -23,65 +23,66 @@
2323
import org.springframework.data.util.Streamable;
2424

2525
/**
26-
* A scroll of data consumed from an underlying query result. A scroll is similar to {@link Slice} in the sense that it
27-
* contains a subset of the actual query results for easier consumption of large result sets. The scroll is less
26+
* A set of data consumed from an underlying query result. A {@link Window} is similar to {@link Slice} in the sense
27+
* that it contains a subset of the actual query results for easier consumption of large result sets. The window is less
2828
* opinionated about the actual data retrieval, whether the query has used index/offset, keyset-based pagination or
2929
* cursor resume tokens.
3030
*
3131
* @author Mark Paluch
32+
* @author Christoph Strobl
3233
* @since 3.1
3334
* @see ScrollPosition
3435
*/
35-
public interface Scroll<T> extends Streamable<T> {
36+
public interface Window<T> extends Streamable<T> {
3637

3738
/**
38-
* Construct a {@link Scroll}.
39+
* Construct a {@link Window}.
3940
*
4041
* @param items the list of data.
4142
* @param positionFunction the list of data.
42-
* @return the {@link Scroll}.
43+
* @return the {@link Window}.
4344
* @param <T>
4445
*/
45-
static <T> Scroll<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction) {
46-
return new ScrollImpl<>(items, positionFunction, false);
46+
static <T> Window<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction) {
47+
return new WindowImpl<>(items, positionFunction, false);
4748
}
4849

4950
/**
50-
* Construct a {@link Scroll}.
51+
* Construct a {@link Window}.
5152
*
5253
* @param items the list of data.
5354
* @param positionFunction the list of data.
5455
* @param hasNext
55-
* @return the {@link Scroll}.
56+
* @return the {@link Window}.
5657
* @param <T>
5758
*/
58-
static <T> Scroll<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
59-
return new ScrollImpl<>(items, positionFunction, hasNext);
59+
static <T> Window<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
60+
return new WindowImpl<>(items, positionFunction, hasNext);
6061
}
6162

6263
/**
63-
* Returns the number of elements in this scroll.
64+
* Returns the number of elements in this window.
6465
*
65-
* @return the number of elements in this scroll.
66+
* @return the number of elements in this window.
6667
*/
6768
int size();
6869

6970
/**
70-
* Returns {@code true} if this scroll contains no elements.
71+
* Returns {@code true} if this window contains no elements.
7172
*
72-
* @return {@code true} if this scroll contains no elements
73+
* @return {@code true} if this window contains no elements
7374
*/
7475
boolean isEmpty();
7576

7677
/**
77-
* Returns the scroll content as {@link List}.
78+
* Returns the windows content as {@link List}.
7879
*
7980
* @return
8081
*/
8182
List<T> getContent();
8283

8384
/**
84-
* Returns whether the current scroll is the last one.
85+
* Returns whether the current window is the last one.
8586
*
8687
* @return
8788
*/
@@ -90,9 +91,9 @@ default boolean isLast() {
9091
}
9192

9293
/**
93-
* Returns if there is a next scroll.
94+
* Returns if there is a next window.
9495
*
95-
* @return if there is a next scroll window.
96+
* @return if there is a next window window.
9697
*/
9798
boolean hasNext();
9899

@@ -123,45 +124,12 @@ default ScrollPosition positionAt(T object) {
123124
return positionAt(index);
124125
}
125126

126-
// TODO: First and last seem to conflict with first/last scroll or first/last position of elements.
127-
// these methods should rather express the position of the first element within this scroll and the scroll position
128-
// to be used to get the next Scroll.
129127
/**
130-
* Returns the first {@link ScrollPosition} or throw {@link NoSuchElementException} if the list is empty.
131-
*
132-
* @return the first {@link ScrollPosition}.
133-
* @throws NoSuchElementException if this result is empty.
134-
*/
135-
default ScrollPosition firstPosition() {
136-
137-
if (size() == 0) {
138-
throw new NoSuchElementException();
139-
}
140-
141-
return positionAt(0);
142-
}
143-
144-
/**
145-
* Returns the last {@link ScrollPosition} or throw {@link NoSuchElementException} if the list is empty.
146-
*
147-
* @return the last {@link ScrollPosition}.
148-
* @throws NoSuchElementException if this result is empty.
149-
*/
150-
default ScrollPosition lastPosition() {
151-
152-
if (size() == 0) {
153-
throw new NoSuchElementException();
154-
}
155-
156-
return positionAt(size() - 1);
157-
}
158-
159-
/**
160-
* Returns a new {@link Scroll} with the content of the current one mapped by the given {@code converter}.
128+
* Returns a new {@link Window} with the content of the current one mapped by the given {@code converter}.
161129
*
162130
* @param converter must not be {@literal null}.
163-
* @return a new {@link Scroll} with the content of the current one mapped by the given {@code converter}.
131+
* @return a new {@link Window} with the content of the current one mapped by the given {@code converter}.
164132
*/
165-
<U> Scroll<U> map(Function<? super T, ? extends U> converter);
133+
<U> Window<U> map(Function<? super T, ? extends U> converter);
166134

167135
}

src/main/java/org/springframework/data/domain/ScrollImpl.java renamed to src/main/java/org/springframework/data/domain/WindowImpl.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,19 @@
2626
import org.springframework.util.ObjectUtils;
2727

2828
/**
29-
* Default {@link Scroll} implementation.
29+
* Default {@link Window} implementation.
3030
*
3131
* @author Mark Paluch
3232
* @since 3.1
3333
*/
34-
class ScrollImpl<T> implements Scroll<T> {
34+
class WindowImpl<T> implements Window<T> {
3535

3636
private final List<T> items;
3737
private final IntFunction<? extends ScrollPosition> positionFunction;
3838

3939
private final boolean hasNext;
4040

41-
ScrollImpl(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
41+
WindowImpl(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
4242

4343
Assert.notNull(items, "List of items must not be null");
4444
Assert.notNull(positionFunction, "Position function must not be null");
@@ -79,11 +79,11 @@ public ScrollPosition positionAt(int index) {
7979
}
8080

8181
@Override
82-
public <U> Scroll<U> map(Function<? super T, ? extends U> converter) {
82+
public <U> Window<U> map(Function<? super T, ? extends U> converter) {
8383

8484
Assert.notNull(converter, "Function must not be null");
8585

86-
return new ScrollImpl<>(stream().map(converter).collect(Collectors.toList()), positionFunction, hasNext);
86+
return new WindowImpl<>(stream().map(converter).collect(Collectors.toList()), positionFunction, hasNext);
8787
}
8888

8989
@NotNull
@@ -98,7 +98,7 @@ public boolean equals(Object o) {
9898
return true;
9999
if (o == null || getClass() != o.getClass())
100100
return false;
101-
ScrollImpl<?> that = (ScrollImpl<?>) o;
101+
WindowImpl<?> that = (WindowImpl<?>) o;
102102
return ObjectUtils.nullSafeEquals(items, that.items)
103103
&& ObjectUtils.nullSafeEquals(positionFunction, that.positionFunction)
104104
&& ObjectUtils.nullSafeEquals(hasNext, that.hasNext);

0 commit comments

Comments
 (0)