Skip to content

Commit 2596c6a

Browse files
committed
Supports scrolling base on keyset without id
id shouldn't be added to sort if sort property already provided. See spring-projectsGH-2996
1 parent d6a895a commit 2596c6a

File tree

6 files changed

+214
-0
lines changed

6 files changed

+214
-0
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java

+5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
*
3939
* @author Mark Paluch
4040
* @author Christoph Strobl
41+
* @author Yanming Zhou
4142
* @since 3.1
4243
*/
4344
public record KeysetScrollSpecification<T> (KeysetScrollPosition position, Sort sort,
@@ -61,6 +62,10 @@ public static Sort createSort(KeysetScrollPosition position, Sort sort, JpaEntit
6162

6263
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection());
6364

65+
if (sort.isSorted()) {
66+
return delegate.getSortOrders(sort);
67+
}
68+
6469
Sort sortToUse;
6570
if (entity.hasCompositeId()) {
6671
sortToUse = sort.and(Sort.by(entity.getIdAttributeNames().toArray(new String[0])));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.Data;
20+
import org.springframework.data.domain.Persistable;
21+
22+
import java.util.UUID;
23+
24+
/**
25+
* @author Yanming Zhou
26+
*/
27+
@Entity
28+
@Data
29+
public class EntityWithoutScrollableId {
30+
31+
private @Id UUID id = UUID.randomUUID();
32+
33+
private @Column(unique = true) int seqNo;
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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.EntityWithoutScrollableId;
28+
import org.springframework.data.jpa.repository.sample.EntityWithoutScrollableIdRepository;
29+
import org.springframework.test.context.ContextConfiguration;
30+
import org.springframework.test.context.junit.jupiter.SpringExtension;
31+
import org.springframework.transaction.annotation.Transactional;
32+
33+
import java.util.ArrayList;
34+
import java.util.List;
35+
import java.util.Map;
36+
import java.util.stream.Stream;
37+
38+
import static org.assertj.core.api.Assertions.assertThat;
39+
40+
/**
41+
* @author Yanming Zhou
42+
*/
43+
@ExtendWith(SpringExtension.class)
44+
@ContextConfiguration("classpath:config/namespace-application-context.xml")
45+
@Transactional
46+
class ScrollingWithoutIdIntegrationTests {
47+
48+
private final static int pageSize = 10;
49+
50+
private final static Integer[] totals = new Integer[] { 0, 5, 10, 15, 20, 25 };
51+
52+
@Autowired
53+
EntityWithoutScrollableIdRepository repository;
54+
55+
void prepare(int total) {
56+
for (int i = 0; i < total; i++) {
57+
EntityWithoutScrollableId entity = new EntityWithoutScrollableId();
58+
entity.setSeqNo(i);
59+
this.repository.save(entity);
60+
}
61+
}
62+
63+
@ParameterizedTest
64+
@MethodSource("cartesian")
65+
void scroll(Sort.Direction sortDirection, ScrollPosition.Direction scrollDirection, int total) {
66+
67+
prepare(total);
68+
69+
List<List<EntityWithoutScrollableId>> contents = new ArrayList<>();
70+
71+
String propertyName = "seqNo";
72+
KeysetScrollPosition position = ScrollPosition.of(Map.of(), scrollDirection);
73+
if (scrollDirection == ScrollPosition.Direction.BACKWARD && position.getDirection() == ScrollPosition.Direction.FORWARD) {
74+
// remove this workaround if https://github.com/spring-projects/spring-data-commons/pull/2841 merged
75+
position = position.backward();
76+
}
77+
while (true) {
78+
ScrollPosition positionToUse = position;
79+
Window<EntityWithoutScrollableId> window = this.repository.findBy((root, query, cb) -> null,
80+
q -> q.limit(pageSize).sortBy(Sort.by(sortDirection, propertyName)).scroll(positionToUse));
81+
if (!window.isEmpty()) {
82+
contents.add(window.getContent());
83+
}
84+
if (!window.hasNext()) {
85+
break;
86+
}
87+
int indexForNext = position.scrollsForward() ? window.size() - 1 : 0;
88+
position = ScrollPosition.of(
89+
Map.of(propertyName,window.getContent().get(indexForNext).getSeqNo()),
90+
scrollDirection);
91+
// position = window.positionAt(indexForNext); https://github.com/spring-projects/spring-data-jpa/pull/3000
92+
// position = window.positionForNext(); https://github.com/spring-projects/spring-data-commons/pull/2843
93+
}
94+
95+
if (total == 0) {
96+
assertThat(contents).hasSize(0);
97+
return;
98+
}
99+
100+
boolean divisible = total % pageSize == 0;
101+
102+
assertThat(contents).hasSize(divisible ? total / pageSize : total / pageSize + 1);
103+
for (int i = 0; i < contents.size() - 1; i++) {
104+
assertThat(contents.get(i)).hasSize(pageSize);
105+
}
106+
if (divisible) {
107+
assertThat(contents.get(contents.size() - 1)).hasSize(pageSize);
108+
}
109+
else {
110+
assertThat(contents.get(contents.size() - 1)).hasSize(total % pageSize);
111+
}
112+
113+
List<EntityWithoutScrollableId> first = contents.get(0);
114+
List<EntityWithoutScrollableId> last = contents.get(contents.size() - 1);
115+
116+
if (sortDirection == Sort.Direction.ASC) {
117+
if (scrollDirection == ScrollPosition.Direction.FORWARD) {
118+
assertThat(first.get(0).getSeqNo()).isEqualTo(0);
119+
assertThat(last.get(last.size() - 1).getSeqNo()).isEqualTo(total - 1);
120+
}
121+
else {
122+
assertThat(first.get(first.size() - 1).getSeqNo()).isEqualTo(total - 1);
123+
assertThat(last.get(0).getSeqNo()).isEqualTo(0);
124+
}
125+
}
126+
else {
127+
if (scrollDirection == ScrollPosition.Direction.FORWARD) {
128+
assertThat(first.get(0).getSeqNo()).isEqualTo(total - 1);
129+
assertThat(last.get(last.size() - 1).getSeqNo()).isEqualTo(0);
130+
}
131+
else {
132+
assertThat(first.get(first.size() - 1).getSeqNo()).isEqualTo(0);
133+
assertThat(last.get(0).getSeqNo()).isEqualTo(total - 1);
134+
}
135+
}
136+
}
137+
138+
private static Stream<Arguments> cartesian() {
139+
return Stream.of(Sort.Direction.class.getEnumConstants())
140+
.flatMap(a -> Stream.of(ScrollPosition.Direction.class.getEnumConstants())
141+
.flatMap(b -> Stream.of(totals).map(total -> Arguments.of(a, b, total))));
142+
}
143+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.EntityWithoutScrollableId;
19+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
20+
import org.springframework.data.repository.CrudRepository;
21+
22+
import java.util.UUID;
23+
24+
/**
25+
* @author Yanming Zhou
26+
*/
27+
public interface EntityWithoutScrollableIdRepository extends CrudRepository<EntityWithoutScrollableId, UUID>, JpaSpecificationExecutor<EntityWithoutScrollableId> {
28+
29+
}

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

+1
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.EntityWithoutScrollableId</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>

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.EntityWithoutScrollableId</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.EntityWithoutScrollableId</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)