Skip to content

Commit 06f9ccd

Browse files
committed
Add KeysetScrollIntegrationTests
1 parent e8d08dd commit 06f9ccd

File tree

5 files changed

+217
-2
lines changed

5 files changed

+217
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2019-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.domain.sample;
17+
18+
import jakarta.persistence.*;
19+
import lombok.*;
20+
import org.springframework.data.jpa.domain.AbstractPersistable;
21+
22+
/**
23+
* @author Yanming Zhou
24+
*/
25+
@Entity
26+
@Setter
27+
@Getter
28+
public class ScrollableEntity extends AbstractPersistable<Integer> {
29+
30+
private @Column(unique = true) int seqNo;
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2019-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository;
17+
18+
import org.junit.jupiter.api.extension.ExtendWith;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.Arguments;
21+
import org.junit.jupiter.params.provider.MethodSource;
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.data.domain.KeysetScrollPosition;
24+
import org.springframework.data.domain.ScrollPosition;
25+
import org.springframework.data.domain.Sort;
26+
import org.springframework.data.domain.Window;
27+
import org.springframework.data.jpa.domain.sample.ScrollableEntity;
28+
import org.springframework.data.jpa.repository.sample.ScrollableEntityRepository;
29+
import org.springframework.lang.Nullable;
30+
import org.springframework.test.context.ContextConfiguration;
31+
import org.springframework.test.context.junit.jupiter.SpringExtension;
32+
import org.springframework.transaction.annotation.Transactional;
33+
34+
import java.util.ArrayList;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.stream.Stream;
38+
39+
import static org.assertj.core.api.Assertions.assertThat;
40+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
41+
42+
/**
43+
* @author Yanming Zhou
44+
*/
45+
@ExtendWith(SpringExtension.class)
46+
@ContextConfiguration("classpath:config/namespace-application-context.xml")
47+
@Transactional
48+
class KeysetScrollIntegrationTests {
49+
50+
private static final int pageSize = 10;
51+
52+
private static final String[][] sortKeys = new String[][] { null, { "id" }, { "seqNo" }, { "id", "seqNo" } };
53+
54+
private static final Integer[] totals = new Integer[] { 0, 5, 10, 15, 20, 25 };
55+
56+
@Autowired
57+
ScrollableEntityRepository repository;
58+
59+
void prepare(int total) {
60+
for (int i = 0; i < total; i++) {
61+
ScrollableEntity entity = new ScrollableEntity();
62+
entity.setSeqNo(i);
63+
this.repository.save(entity);
64+
}
65+
}
66+
67+
@ParameterizedTest
68+
@MethodSource("cartesian")
69+
void scroll(@Nullable String[] keys, Sort.Direction sortDirection, ScrollPosition.Direction scrollDirection, int total) {
70+
71+
prepare(total);
72+
73+
List<List<ScrollableEntity>> contents = new ArrayList<>();
74+
75+
Sort sort;
76+
if (keys != null) {
77+
sort = Sort.by(sortDirection, keys);
78+
}
79+
else {
80+
sort = Sort.unsorted();
81+
// implicit "id:ASC" will be used
82+
assumeTrue(sortDirection == Sort.Direction.ASC);
83+
}
84+
KeysetScrollPosition position = ScrollPosition.of(Map.of(), scrollDirection);
85+
if (scrollDirection == ScrollPosition.Direction.BACKWARD && position.getDirection() == ScrollPosition.Direction.FORWARD) {
86+
// remove this workaround if https://github.com/spring-projects/spring-data-commons/pull/2841 merged
87+
position = position.backward();
88+
}
89+
while (true) {
90+
ScrollPosition positionToUse = position;
91+
Window<ScrollableEntity> window = this.repository.findBy((root, query, cb) -> null,
92+
q -> q.limit(pageSize).sortBy(sort).scroll(positionToUse));
93+
if (!window.isEmpty()) {
94+
contents.add(window.getContent());
95+
}
96+
if (!window.hasNext()) {
97+
break;
98+
}
99+
int indexForNext = position.scrollsForward() ? window.size() - 1 : 0;
100+
position = (KeysetScrollPosition) window.positionAt(indexForNext);
101+
// position = window.positionForNext(); https://github.com/spring-projects/spring-data-commons/pull/2843
102+
}
103+
104+
if (total == 0) {
105+
assertThat(contents).hasSize(0);
106+
return;
107+
}
108+
109+
boolean divisible = total % pageSize == 0;
110+
111+
assertThat(contents).hasSize(divisible ? total / pageSize : total / pageSize + 1);
112+
for (int i = 0; i < contents.size() - 1; i++) {
113+
assertThat(contents.get(i)).hasSize(pageSize);
114+
}
115+
if (divisible) {
116+
assertThat(contents.get(contents.size() - 1)).hasSize(pageSize);
117+
}
118+
else {
119+
assertThat(contents.get(contents.size() - 1)).hasSize(total % pageSize);
120+
}
121+
122+
List<ScrollableEntity> first = contents.get(0);
123+
List<ScrollableEntity> last = contents.get(contents.size() - 1);
124+
125+
if (sortDirection == Sort.Direction.ASC) {
126+
if (scrollDirection == ScrollPosition.Direction.FORWARD) {
127+
assertThat(first.get(0).getSeqNo()).isEqualTo(0);
128+
assertThat(last.get(last.size() - 1).getSeqNo()).isEqualTo(total - 1);
129+
}
130+
else {
131+
assertThat(first.get(first.size() - 1).getSeqNo()).isEqualTo(total - 1);
132+
assertThat(last.get(0).getSeqNo()).isEqualTo(0);
133+
}
134+
}
135+
else {
136+
if (scrollDirection == ScrollPosition.Direction.FORWARD) {
137+
assertThat(first.get(0).getSeqNo()).isEqualTo(total - 1);
138+
assertThat(last.get(last.size() - 1).getSeqNo()).isEqualTo(0);
139+
}
140+
else {
141+
assertThat(first.get(first.size() - 1).getSeqNo()).isEqualTo(0);
142+
assertThat(last.get(0).getSeqNo()).isEqualTo(total - 1);
143+
}
144+
}
145+
}
146+
147+
private static Stream<Arguments> cartesian() {
148+
return Stream.of(sortKeys)
149+
.flatMap(keys -> Stream.of(Sort.Direction.class.getEnumConstants())
150+
.flatMap(sortDirection -> Stream.of(ScrollPosition.Direction.class.getEnumConstants())
151+
.flatMap(scrollDirection -> Stream.of(totals)
152+
.map(total -> Arguments.of(keys, sortDirection, scrollDirection, total)))));
153+
}
154+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2019-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.sample;
17+
18+
import org.springframework.data.jpa.domain.sample.ScrollableEntity;
19+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
20+
import org.springframework.data.repository.CrudRepository;
21+
22+
/**
23+
* @author Yanming Zhou
24+
*/
25+
public interface ScrollableEntityRepository extends CrudRepository<ScrollableEntity, Integer>, JpaSpecificationExecutor<ScrollableEntity> {
26+
27+
}

spring-data-jpa/src/test/resources/META-INF/persistence.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<class>org.springframework.data.jpa.domain.sample.ConcreteType2</class>
1919
<class>org.springframework.data.jpa.domain.sample.CustomAbstractPersistable</class>
2020
<class>org.springframework.data.jpa.domain.sample.Customer</class>
21+
<class>org.springframework.data.jpa.domain.sample.ScrollableEntity</class>
2122
<class>org.springframework.data.jpa.domain.sample.EntityWithAssignedId</class>
2223
<class>org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployeePK</class>
2324
<class>org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployee</class>
@@ -35,8 +36,7 @@
3536
<class>org.springframework.data.jpa.domain.sample.Order</class>
3637
<class>org.springframework.data.jpa.domain.sample.Parent</class>
3738
<class>org.springframework.data.jpa.domain.sample.PersistableWithIdClass</class>
38-
<class>org.springframework.data.jpa.domain.sample.PersistableWithSingleIdClass
39-
</class>
39+
<class>org.springframework.data.jpa.domain.sample.PersistableWithSingleIdClass</class>
4040
<class>org.springframework.data.jpa.domain.sample.PrimitiveVersionProperty</class>
4141
<class>org.springframework.data.jpa.domain.sample.Product</class>
4242
<class>org.springframework.data.jpa.domain.sample.Role</class>

spring-data-jpa/src/test/resources/META-INF/persistence2.xml

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<class>org.springframework.data.jpa.domain.sample.AuditableEmbeddable</class>
1111
<class>org.springframework.data.jpa.domain.sample.Category</class>
1212
<class>org.springframework.data.jpa.domain.sample.CustomAbstractPersistable</class>
13+
<class>org.springframework.data.jpa.domain.sample.ScrollableEntity</class>
1314
<class>org.springframework.data.jpa.domain.sample.EntityWithAssignedId</class>
1415
<class>org.springframework.data.jpa.domain.sample.Item</class>
1516
<class>org.springframework.data.jpa.domain.sample.ItemSite</class>
@@ -30,6 +31,7 @@
3031
<class>org.springframework.data.jpa.domain.sample.AuditableRole</class>
3132
<class>org.springframework.data.jpa.domain.sample.Category</class>
3233
<class>org.springframework.data.jpa.domain.sample.CustomAbstractPersistable</class>
34+
<class>org.springframework.data.jpa.domain.sample.ScrollableEntity</class>
3335
<class>org.springframework.data.jpa.domain.sample.EntityWithAssignedId</class>
3436
<class>org.springframework.data.jpa.domain.sample.Item</class>
3537
<class>org.springframework.data.jpa.domain.sample.ItemSite</class>

0 commit comments

Comments
 (0)