Skip to content

Commit eb55c0d

Browse files
christophstroblapp
authored and
app
committed
Revise OffsetScrollPosition to use zero-based indexesAlign Offset Scrolling Position.
Closes spring-projects#3070 Original pull request: spring-projects#3072
1 parent 055d4a8 commit eb55c0d

File tree

7 files changed

+87
-34
lines changed

7 files changed

+87
-34
lines changed

Diff for: src/main/antora/modules/ROOT/pages/repositories/scrolling.adoc

+17-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ 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 xref:repositories/query-methods-details.adoc#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 `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.
9+
Scroll queries return a `Window<T>` that allows obtaining the elements scroll position which can be used to fetch the next `Window<T>` until your application has consumed the entire query result.
1010
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(...)`.
1111

1212
[source,java]
@@ -23,12 +23,19 @@ do {
2323
} while (!users.isEmpty() && users.hasNext());
2424
----
2525

26+
[NOTE]
27+
====
28+
The `ScrollPosition` identifies the exact position of an element with the entire query result.
29+
Query execution treats the position parameter as _exclusive_, which means results will start _after_ the given position.
30+
`ScrollPosition#offset` and `ScrollPosition#keyset()` as special incarnations of a `ScrollPosition` indicating the start of a scroll operation.
31+
====
32+
2633
`WindowIterator` provides a utility to simplify scrolling across ``Window``s by removing the need to check for the presence of a next `Window` and applying the `ScrollPosition`.
2734

2835
[source,java]
2936
----
3037
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
31-
.startingAt(OffsetScrollPosition.initial());
38+
.startingAt(ScrollPosition.offset());
3239
3340
while (users.hasNext()) {
3441
User u = users.next();
@@ -56,7 +63,14 @@ WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst1
5663
.startingAt(OffsetScrollPosition.initial()); <1>
5764
----
5865
59-
<1> Start from the initial offset at position `0`.
66+
<1> Start with no offset to include the element at position `0`.
67+
====
68+
69+
[CAUTION]
70+
====
71+
There is a difference between `ScollPosition.offset()` and `ScollPosition.offset(0L)`.
72+
The former indicates the start of scroll operation, pointing to no specific offset where as the latter identifies the first element (at position `0`) of the result.
73+
Given the _exclusive_ nature of scrolling using `ScollPosition.offset(0)` will skip the first element and translate to an offset of 1.
6074
====
6175

6276
[[repositories.scrolling.keyset]]

Diff for: src/main/java/org/springframework/data/domain/OffsetScrollPosition.java

+17-9
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,28 @@
2323

2424
/**
2525
* A {@link ScrollPosition} based on the offsets within query results.
26+
* <p>
27+
* An initial {@link OffsetScrollPosition} does not point to a specific element and is different to a the Po
2628
*
2729
* @author Mark Paluch
2830
* @author Oliver Drotbohm
31+
* @author Christoph Strobl
2932
* @since 3.1
3033
*/
3134
public final class OffsetScrollPosition implements ScrollPosition {
3235

33-
private static final OffsetScrollPosition INITIAL = new OffsetScrollPosition(0);
36+
private static final OffsetScrollPosition INITIAL = new OffsetScrollPosition(null);
3437

35-
private final long offset;
38+
@Nullable private final Long offset;
3639

3740
/**
3841
* Creates a new {@link OffsetScrollPosition} for the given non-negative offset.
3942
*
4043
* @param offset must be greater or equal to zero.
4144
*/
42-
private OffsetScrollPosition(long offset) {
45+
private OffsetScrollPosition(@Nullable Long offset) {
4346

44-
Assert.isTrue(offset >= 0, "Offset must not be negative");
47+
Assert.isTrue(offset == null || offset >= 0, "Offset must not be negative");
4548

4649
this.offset = offset;
4750
}
@@ -62,7 +65,7 @@ static OffsetScrollPosition initial() {
6265
* @return will never be {@literal null}.
6366
*/
6467
static OffsetScrollPosition of(long offset) {
65-
return offset == 0 ? initial() : new OffsetScrollPosition(offset);
68+
return new OffsetScrollPosition(offset);
6669
}
6770

6871
/**
@@ -80,10 +83,15 @@ public static IntFunction<OffsetScrollPosition> positionFunction(long startOffse
8083

8184
/**
8285
* The zero or positive offset.
86+
* <p>
87+
* An {@link #isInitial() initial} position does not define an offset and will raise an error.
8388
*
8489
* @return the offset.
90+
* @throws IllegalStateException if {@link #isInitial()}.
8591
*/
8692
public long getOffset() {
93+
94+
Assert.state(offset != null, "Initial state does not have an offset. Make sure to check #isInitial()");
8795
return offset;
8896
}
8997

@@ -96,14 +104,14 @@ public long getOffset() {
96104
*/
97105
public OffsetScrollPosition advanceBy(long delta) {
98106

99-
var value = offset + delta;
107+
var value = isInitial() ? delta : offset + delta;
100108

101109
return new OffsetScrollPosition(value < 0 ? 0 : value);
102110
}
103111

104112
@Override
105113
public boolean isInitial() {
106-
return offset == 0;
114+
return offset == null;
107115
}
108116

109117
@Override
@@ -117,7 +125,7 @@ public boolean equals(@Nullable Object o) {
117125
return false;
118126
}
119127

120-
return offset == that.offset;
128+
return Objects.equals(offset, that.offset);
121129
}
122130

123131
@Override
@@ -141,7 +149,7 @@ public OffsetScrollPosition apply(int offset) {
141149
throw new IndexOutOfBoundsException(offset);
142150
}
143151

144-
return of(startOffset + offset + 1);
152+
return of(startOffset + offset);
145153
}
146154
}
147155
}

Diff for: src/main/java/org/springframework/data/domain/Pageable.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,12 @@ default Limit toLimit() {
191191

192192
/**
193193
* Returns an {@link OffsetScrollPosition} from this pageable if the page request {@link #isPaged() is paged}.
194+
* <p>
195+
* Given the exclusive nature of scrolling the {@link ScrollPosition} for {@code Page(0, 10)} translates an
196+
* {@link ScrollPosition#isInitial() initial} position, where as {@code Page(1, 10)} will point to the last element of
197+
* {@code Page(0,10)} resulting in {@link ScrollPosition#offset(long) ScrollPosition(9)}.
194198
*
195-
* @return
199+
* @return new instance of {@link OffsetScrollPosition}.
196200
* @throws IllegalStateException if the request is {@link #isUnpaged()}
197201
* @since 3.1
198202
*/
@@ -202,6 +206,7 @@ default OffsetScrollPosition toScrollPosition() {
202206
throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance");
203207
}
204208

205-
return ScrollPosition.offset(getOffset());
209+
return getOffset() > 0 ? ScrollPosition.offset(getOffset() - 1 /* scrolling is exclusive */)
210+
: ScrollPosition.offset();
206211
}
207212
}

Diff for: src/test/java/org/springframework/data/domain/AbstractPageRequestUnitTests.java

+7
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,11 @@ void getOffsetShouldNotCauseOverflow() {
8484

8585
assertThat(request.getOffset()).isGreaterThan(Integer.MAX_VALUE);
8686
}
87+
88+
@Test // GH-2151, GH-3070
89+
void createsOffsetScrollPosition() {
90+
91+
assertThat(newPageRequest(0, 10).toScrollPosition()).returns(true, ScrollPosition::isInitial);
92+
assertThat(newPageRequest(1, 10).toScrollPosition()).returns(9L, OffsetScrollPosition::getOffset);
93+
}
8794
}

Diff for: src/test/java/org/springframework/data/domain/PageRequestUnitTests.java

-8
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,4 @@ void rejectsNullSort() {
7171
assertThatIllegalArgumentException() //
7272
.isThrownBy(() -> PageRequest.of(0, 10, null));
7373
}
74-
75-
@Test // GH-2151
76-
void createsOffsetScrollPosition() {
77-
78-
PageRequest request = PageRequest.of(1, 10);
79-
80-
assertThat(request.toScrollPosition()).isEqualTo(ScrollPosition.offset(10));
81-
}
8274
}

Diff for: src/test/java/org/springframework/data/domain/ScrollPositionUnitTests.java

+34-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*
3030
* @author Mark Paluch
3131
* @author Oliver Drotbohm
32+
* @author Christoph Strobl
3233
*/
3334
class ScrollPositionUnitTests {
3435

@@ -56,14 +57,14 @@ void equalsAndHashCodeForOffsets() {
5657
assertThat(foo1).isNotEqualTo(bar).doesNotHaveSameHashCodeAs(bar);
5758
}
5859

59-
@Test // GH-2151
60+
@Test // GH-2151, GH-3070
6061
void shouldCreateCorrectIndexPosition() {
6162

62-
assertThat(positionFunction(0).apply(0)).isEqualTo(ScrollPosition.offset(1));
63-
assertThat(positionFunction(0).apply(1)).isEqualTo(ScrollPosition.offset(2));
63+
assertThat(positionFunction(0).apply(0)).isEqualTo(ScrollPosition.offset(0));
64+
assertThat(positionFunction(0).apply(1)).isEqualTo(ScrollPosition.offset(1));
6465

65-
assertThat(positionFunction(100).apply(0)).isEqualTo(ScrollPosition.offset(101));
66-
assertThat(positionFunction(100).apply(1)).isEqualTo(ScrollPosition.offset(102));
66+
assertThat(positionFunction(100).apply(0)).isEqualTo(ScrollPosition.offset(100));
67+
assertThat(positionFunction(100).apply(1)).isEqualTo(ScrollPosition.offset(101));
6768
}
6869

6970
@Test // GH-2151
@@ -80,6 +81,23 @@ void advanceOffsetBelowZeroCapsAtZero() {
8081
assertThat(offset.advanceBy(-10)).isEqualTo(ScrollPosition.offset(0));
8182
}
8283

84+
@Test // GH-3070
85+
void advanceOffsetForward() {
86+
87+
OffsetScrollPosition offset = ScrollPosition.offset(5);
88+
89+
assertThat(offset.getOffset()).isEqualTo(5);
90+
assertThat(offset.advanceBy(5)).isEqualTo(ScrollPosition.offset(10));
91+
}
92+
93+
@Test // GH-3070
94+
void advanceInitialOffsetForward() {
95+
96+
OffsetScrollPosition offset = ScrollPosition.offset();
97+
98+
assertThat(offset.advanceBy(5)).isEqualTo(ScrollPosition.offset(5));
99+
}
100+
83101
@Test // GH-2824
84102
void setsUpForwardScrolling() {
85103

@@ -120,13 +138,22 @@ void setsUpBackwardScrolling() {
120138
assertThat(position.reverse()).isEqualTo(forward);
121139
}
122140

123-
@Test // GH-2824
141+
@Test // GH-2824, GH-3070
124142
void initialOffsetPosition() {
125143

126144
OffsetScrollPosition position = ScrollPosition.offset();
127145

128146
assertThat(position.isInitial()).isTrue();
129-
assertThat(position.getOffset()).isEqualTo(0);
147+
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(position::getOffset);
148+
}
149+
150+
@Test // GH-3070
151+
void initialOffsetPositionIsNotEqualToPositionOfFirstElement() {
152+
153+
OffsetScrollPosition first = ScrollPosition.offset(0);
154+
155+
assertThat(first.isInitial()).isFalse();
156+
assertThat(first).isNotEqualTo(ScrollPosition.offset());
130157
}
131158

132159
@Test // GH-2824, GH-2840

Diff for: src/test/java/org/springframework/data/domain/WindowUnitTests.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,18 @@ void allowsIteration() {
5353
}
5454
}
5555

56-
@Test // GH-2151
56+
@Test // GH-2151, GH-3070
5757
void shouldCreateCorrectPositions() {
5858

5959
Window<Integer> window = Window.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0));
6060

61-
assertThat(window.positionAt(0)).isEqualTo(ScrollPosition.offset(1));
62-
assertThat(window.positionAt(window.size() - 1)).isEqualTo(ScrollPosition.offset(3));
61+
assertThat(window.positionAt(0)).isEqualTo(ScrollPosition.offset(0));
62+
assertThat(window.positionAt(window.size() - 1)).isEqualTo(ScrollPosition.offset(2));
6363

6464
// by index
65-
assertThat(window.positionAt(1)).isEqualTo(ScrollPosition.offset(2));
65+
assertThat(window.positionAt(1)).isEqualTo(ScrollPosition.offset(1));
6666

6767
// by object
68-
assertThat(window.positionAt(Integer.valueOf(1))).isEqualTo(ScrollPosition.offset(1));
68+
assertThat(window.positionAt(Integer.valueOf(1))).isEqualTo(ScrollPosition.offset(0));
6969
}
7070
}

0 commit comments

Comments
 (0)