Skip to content

Commit bd1c670

Browse files
committed
Add support for fluent limit(int) and scroll(OffsetScrollPosition) to Query by Example queries.
Closes #1609
1 parent 6d1d966 commit bd1c670

File tree

8 files changed

+387
-70
lines changed

8 files changed

+387
-70
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java

+49-10
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,32 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.support;
1717

18+
import java.util.ArrayList;
19+
import java.util.Collection;
1820
import java.util.Collections;
1921
import java.util.List;
22+
import java.util.function.Function;
2023
import java.util.function.UnaryOperator;
21-
import java.util.stream.Collectors;
2224
import java.util.stream.Stream;
2325
import java.util.stream.StreamSupport;
2426

2527
import org.springframework.data.domain.Example;
28+
import org.springframework.data.domain.OffsetScrollPosition;
2629
import org.springframework.data.domain.Page;
2730
import org.springframework.data.domain.Pageable;
31+
import org.springframework.data.domain.ScrollPosition;
2832
import org.springframework.data.domain.Sort;
33+
import org.springframework.data.domain.Window;
2934
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
3035
import org.springframework.data.relational.core.query.Query;
3136
import org.springframework.data.relational.repository.query.RelationalExampleMapper;
37+
import org.springframework.util.Assert;
3238

3339
/**
3440
* {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using {@link Example}.
3541
*
3642
* @author Diego Krupitza
43+
* @author Mark Paluch
3744
* @since 3.0
3845
*/
3946
class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
@@ -43,13 +50,13 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
4350

4451
FetchableFluentQueryByExample(Example<S> example, Class<R> resultType, RelationalExampleMapper exampleMapper,
4552
JdbcAggregateOperations entityOperations) {
46-
this(example, Sort.unsorted(), resultType, Collections.emptyList(), exampleMapper, entityOperations);
53+
this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations);
4754
}
4855

49-
FetchableFluentQueryByExample(Example<S> example, Sort sort, Class<R> resultType, List<String> fieldsToInclude,
50-
RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) {
56+
FetchableFluentQueryByExample(Example<S> example, Sort sort, int limit, Class<R> resultType,
57+
List<String> fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) {
5158

52-
super(example, sort, resultType, fieldsToInclude);
59+
super(example, sort, limit, resultType, fieldsToInclude);
5360

5461
this.exampleMapper = exampleMapper;
5562
this.entityOperations = entityOperations;
@@ -71,10 +78,40 @@ public R firstValue() {
7178

7279
@Override
7380
public List<R> all() {
81+
return findAll(createQuery().sort(getSort()));
82+
}
7483

75-
return StreamSupport
76-
.stream(this.entityOperations.findAll(createQuery().sort(getSort()), getExampleType()).spliterator(), false)
77-
.map(item -> this.getConversionFunction().apply(item)).collect(Collectors.toList());
84+
private List<R> findAll(Query query) {
85+
86+
Function<Object, R> conversionFunction = this.getConversionFunction();
87+
Iterable<S> raw = this.entityOperations.findAll(query, getExampleType());
88+
89+
List<R> result = new ArrayList<>(raw instanceof Collections ? ((Collection<?>) raw).size() : 16);
90+
91+
for (S s : raw) {
92+
result.add(conversionFunction.apply(s));
93+
}
94+
95+
return result;
96+
}
97+
98+
@Override
99+
public Window<R> scroll(ScrollPosition scrollPosition) {
100+
101+
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
102+
103+
if (scrollPosition instanceof OffsetScrollPosition osp) {
104+
105+
Query query = createQuery().sort(getSort()).offset(osp.getOffset());
106+
107+
if (getLimit() > 0) {
108+
query = query.limit(getLimit());
109+
}
110+
111+
return ScrollDelegate.scroll(query, this::findAll, osp);
112+
}
113+
114+
return super.scroll(scrollPosition);
78115
}
79116

80117
@Override
@@ -114,16 +151,18 @@ private Query createQuery(UnaryOperator<Query> queryCustomizer) {
114151
query = query.columns(getFieldsToInclude().toArray(new String[0]));
115152
}
116153

154+
query = query.limit(getLimit());
155+
117156
query = queryCustomizer.apply(query);
118157

119158
return query;
120159
}
121160

122161
@Override
123-
protected <R> FluentQuerySupport<S, R> create(Example<S> example, Sort sort, Class<R> resultType,
162+
protected <R> FluentQuerySupport<S, R> create(Example<S> example, Sort sort, int limit, Class<R> resultType,
124163
List<String> fieldsToInclude) {
125164

126-
return new FetchableFluentQueryByExample<>(example, sort, resultType, fieldsToInclude, this.exampleMapper,
165+
return new FetchableFluentQueryByExample<>(example, sort, limit, resultType, fieldsToInclude, this.exampleMapper,
127166
this.entityOperations);
128167
}
129168
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java

+25-10
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,40 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.support;
1717

18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.List;
21+
import java.util.function.Function;
22+
1823
import org.springframework.core.convert.support.DefaultConversionService;
1924
import org.springframework.data.domain.Example;
2025
import org.springframework.data.domain.Sort;
2126
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
2227
import org.springframework.data.repository.query.FluentQuery;
2328
import org.springframework.util.Assert;
2429

25-
import java.util.ArrayList;
26-
import java.util.Collection;
27-
import java.util.List;
28-
import java.util.function.Function;
29-
3030
/**
3131
* Support class for {@link FluentQuery.FetchableFluentQuery} implementations.
3232
*
3333
* @author Diego Krupitza
34+
* @author Mark Paluch
3435
* @since 3.0
3536
*/
3637
abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQuery<R> {
3738

3839
private final Example<S> example;
3940
private final Sort sort;
41+
private final int limit;
4042
private final Class<R> resultType;
4143
private final List<String> fieldsToInclude;
4244

4345
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
4446

45-
FluentQuerySupport(Example<S> example, Sort sort, Class<R> resultType, List<String> fieldsToInclude) {
47+
FluentQuerySupport(Example<S> example, Sort sort, int limit, Class<R> resultType, List<String> fieldsToInclude) {
4648

4749
this.example = example;
4850
this.sort = sort;
51+
this.limit = limit;
4952
this.resultType = resultType;
5053
this.fieldsToInclude = fieldsToInclude;
5154
}
@@ -55,26 +58,34 @@ public FetchableFluentQuery<R> sortBy(Sort sort) {
5558

5659
Assert.notNull(sort, "Sort must not be null!");
5760

58-
return create(example, sort, resultType, fieldsToInclude);
61+
return create(example, sort, limit, resultType, fieldsToInclude);
62+
}
63+
64+
@Override
65+
public FetchableFluentQuery<R> limit(int limit) {
66+
67+
Assert.isTrue(limit >= 0, "Limit must not be negative");
68+
69+
return create(example, sort, limit, resultType, fieldsToInclude);
5970
}
6071

6172
@Override
6273
public <R> FetchableFluentQuery<R> as(Class<R> projection) {
6374

6475
Assert.notNull(projection, "Projection target type must not be null!");
6576

66-
return create(example, sort, projection, fieldsToInclude);
77+
return create(example, sort, limit, projection, fieldsToInclude);
6778
}
6879

6980
@Override
7081
public FetchableFluentQuery<R> project(Collection<String> properties) {
7182

7283
Assert.notNull(properties, "Projection properties must not be null!");
7384

74-
return create(example, sort, resultType, new ArrayList<>(properties));
85+
return create(example, sort, limit, resultType, new ArrayList<>(properties));
7586
}
7687

77-
protected abstract <R> FluentQuerySupport<S, R> create(Example<S> example, Sort sort, Class<R> resultType,
88+
protected abstract <R> FluentQuerySupport<S, R> create(Example<S> example, Sort sort, int limit, Class<R> resultType,
7889
List<String> fieldsToInclude);
7990

8091
Class<S> getExampleType() {
@@ -89,6 +100,10 @@ Sort getSort() {
89100
return sort;
90101
}
91102

103+
int getLimit() {
104+
return limit;
105+
}
106+
92107
Class<R> getResultType() {
93108
return resultType;
94109
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 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.jdbc.repository.support;
17+
18+
import java.util.List;
19+
import java.util.function.Function;
20+
import java.util.function.IntFunction;
21+
22+
import org.springframework.data.domain.OffsetScrollPosition;
23+
import org.springframework.data.domain.ScrollPosition;
24+
import org.springframework.data.domain.Window;
25+
import org.springframework.data.relational.core.query.Query;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* Delegate to run {@link ScrollPosition scroll queries} and create result {@link Window}.
30+
*
31+
* @author Mark Paluch
32+
* @since 3.1.4
33+
*/
34+
public class ScrollDelegate {
35+
36+
/**
37+
* Run the {@link Query} and return a scroll {@link Window}.
38+
*
39+
* @param query must not be {@literal null}.
40+
* @param scrollPosition must not be {@literal null}.
41+
* @return the scroll {@link Window}.
42+
*/
43+
@SuppressWarnings("unchecked")
44+
public static <T> Window<T> scroll(Query query, Function<Query, List<T>> queryFunction,
45+
ScrollPosition scrollPosition) {
46+
47+
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
48+
49+
int limit = query.getLimit();
50+
if (limit > 0 && limit != Integer.MAX_VALUE) {
51+
query = query.limit(limit + 1);
52+
}
53+
54+
List<T> result = queryFunction.apply(query);
55+
56+
if (scrollPosition instanceof OffsetScrollPosition offset) {
57+
return createWindow(result, limit, OffsetScrollPosition.positionFunction(offset.getOffset()));
58+
}
59+
60+
throw new UnsupportedOperationException("ScrollPosition " + scrollPosition + " not supported");
61+
}
62+
63+
private static <T> Window<T> createWindow(List<T> result, int limit,
64+
IntFunction<? extends ScrollPosition> positionFunction) {
65+
return Window.from(getFirst(limit, result), positionFunction, hasMoreElements(result, limit));
66+
}
67+
68+
private static boolean hasMoreElements(List<?> result, int limit) {
69+
return !result.isEmpty() && result.size() > limit;
70+
}
71+
72+
/**
73+
* Return the first {@code count} items from the list.
74+
*
75+
* @param count the number of first elements to be included in the returned list.
76+
* @param list must not be {@literal null}
77+
* @return the returned sublist if the {@code list} is greater {@code count}.
78+
* @param <T> the element type of the lists.
79+
*/
80+
public static <T> List<T> getFirst(int count, List<T> list) {
81+
82+
if (count > 0 && list.size() > count) {
83+
return list.subList(0, count);
84+
}
85+
86+
return list;
87+
}
88+
89+
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

+48-5
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,14 @@
5353
import org.springframework.dao.IncorrectResultSizeDataAccessException;
5454
import org.springframework.data.annotation.Id;
5555
import org.springframework.data.domain.Example;
56+
import org.springframework.data.domain.ExampleMatcher;
5657
import org.springframework.data.domain.Page;
5758
import org.springframework.data.domain.PageRequest;
5859
import org.springframework.data.domain.Pageable;
60+
import org.springframework.data.domain.ScrollPosition;
5961
import org.springframework.data.domain.Slice;
6062
import org.springframework.data.domain.Sort;
63+
import org.springframework.data.domain.Window;
6164
import org.springframework.data.jdbc.core.mapping.AggregateReference;
6265
import org.springframework.data.jdbc.repository.query.Modifying;
6366
import org.springframework.data.jdbc.repository.query.Query;
@@ -82,6 +85,8 @@
8285
import org.springframework.data.repository.query.Param;
8386
import org.springframework.data.repository.query.QueryByExampleExecutor;
8487
import org.springframework.data.spel.spi.EvaluationContextExtension;
88+
import org.springframework.data.support.WindowIterator;
89+
import org.springframework.data.util.Streamable;
8590
import org.springframework.jdbc.core.JdbcTemplate;
8691
import org.springframework.jdbc.core.RowMapper;
8792
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@@ -1073,8 +1078,6 @@ void fetchByExampleFluentAllSimple() {
10731078
String searchName = "Diego";
10741079
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
10751080

1076-
final DummyEntity one = repository.save(createDummyEntity());
1077-
10781081
DummyEntity two = createDummyEntity();
10791082

10801083
two.setName(searchName);
@@ -1101,6 +1104,42 @@ void fetchByExampleFluentAllSimple() {
11011104
assertThat(matches).containsExactly(two, third);
11021105
}
11031106

1107+
@Test // GH-1609
1108+
void findByScrollPosition() {
1109+
1110+
DummyEntity one = new DummyEntity("one");
1111+
one.setFlag(true);
1112+
1113+
DummyEntity two = new DummyEntity("two");
1114+
two.setFlag(true);
1115+
1116+
DummyEntity three = new DummyEntity("three");
1117+
three.setFlag(true);
1118+
1119+
DummyEntity four = new DummyEntity("four");
1120+
four.setFlag(false);
1121+
1122+
repository.saveAll(Arrays.asList(one, two, three, four));
1123+
1124+
Example<DummyEntity> example = Example.of(one, ExampleMatcher.matching().withIgnorePaths("name", "idProp"));
1125+
1126+
Window<DummyEntity> first = repository.findBy(example, q -> q.limit(2).sortBy(Sort.by("name")))
1127+
.scroll(ScrollPosition.offset());
1128+
assertThat(first.map(DummyEntity::getName)).containsExactly("one", "three");
1129+
1130+
Window<DummyEntity> second = repository.findBy(example, q -> q.limit(2).sortBy(Sort.by("name")))
1131+
.scroll(ScrollPosition.offset(2));
1132+
assertThat(second.map(DummyEntity::getName)).containsExactly("two");
1133+
1134+
WindowIterator<DummyEntity> iterator = WindowIterator.of(
1135+
scrollPosition -> repository.findBy(example, q -> q.limit(2).sortBy(Sort.by("name")).scroll(scrollPosition)))
1136+
.startingAt(ScrollPosition.offset());
1137+
1138+
List<String> result = Streamable.of(() -> iterator).stream().map(DummyEntity::getName).toList();
1139+
1140+
assertThat(result).hasSize(3).containsExactly("one", "three", "two");
1141+
}
1142+
11041143
@Test // GH-1192
11051144
void fetchByExampleFluentCountSimple() {
11061145

@@ -1777,10 +1816,14 @@ public void setDirection(Direction direction) {
17771816

17781817
@Override
17791818
public boolean equals(Object o) {
1780-
if (this == o) return true;
1781-
if (o == null || getClass() != o.getClass()) return false;
1819+
if (this == o)
1820+
return true;
1821+
if (o == null || getClass() != o.getClass())
1822+
return false;
17821823
DummyEntity that = (DummyEntity) o;
1783-
return flag == that.flag && Objects.equals(name, that.name) && Objects.equals(pointInTime, that.pointInTime) && Objects.equals(offsetDateTime, that.offsetDateTime) && Objects.equals(idProp, that.idProp) && Objects.equals(ref, that.ref) && direction == that.direction;
1824+
return flag == that.flag && Objects.equals(name, that.name) && Objects.equals(pointInTime, that.pointInTime)
1825+
&& Objects.equals(offsetDateTime, that.offsetDateTime) && Objects.equals(idProp, that.idProp)
1826+
&& Objects.equals(ref, that.ref) && direction == that.direction;
17841827
}
17851828

17861829
@Override

0 commit comments

Comments
 (0)