Skip to content

Commit 515a1b0

Browse files
Artemiy Degtyarevchrshnv
authored andcommitted
add: offset scrolling support
Signed-off-by: Artemiy Degtyarev <[email protected]> Signed-off-by: Artemiy Chereshnevvv <[email protected]>
1 parent f3256b2 commit 515a1b0

File tree

6 files changed

+98
-31
lines changed

6 files changed

+98
-31
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/aot/JdbcCodeBlocks.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ private CodeBlock select(ExecutionDecorator decorator, String rowMapper, String
779779
boolean dynamicProjection = StringUtils.hasText(context.getDynamicProjectionParameterName());
780780
Object queryResultTypeRef = dynamicProjection ? context.getDynamicProjectionParameterName() : queryResultType;
781781

782-
if (queryMethod.isCollectionQuery() || queryMethod.isSliceQuery() || queryMethod.isPageQuery()) {
782+
if (queryMethod.isCollectionQuery() || queryMethod.isSliceQuery() || queryMethod.isPageQuery() || queryMethod.isScrollQuery()) {
783783

784784
builder.addStatement(
785785
"$1T $2L = ($1T) " + decorator.decorate("getJdbcOperations().query($3L, $4L, new $5T<>($6L))"), List.class,

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ public JdbcCountQueryCreator(PartTree tree, JdbcConverter converter, Dialect dia
4444

4545
JdbcCountQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
4646
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
47-
ReturnedType returnedType, Optional<Lock> lockMode) {
48-
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType, lockMode);
47+
ReturnedType returnedType, Optional<Lock> lockMode, boolean isScrollQuery) {
48+
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType, lockMode, isScrollQuery);
4949
}
5050

5151
@Override

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery>
5959
private final ReturnedType returnedType;
6060
private final Optional<Lock> lockMode;
6161
private final StatementFactory statementFactory;
62+
private final boolean isScrollQuery;
6263

6364
/**
6465
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
@@ -73,15 +74,15 @@ public class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery>
7374
* @param isSliceQuery flag denoting if the query returns a {@link org.springframework.data.domain.Slice}.
7475
* @param returnedType the {@link ReturnedType} to be returned by the query. Must not be {@literal null}.
7576
* @deprecated use
76-
* {@link JdbcQueryCreator#JdbcQueryCreator(RelationalMappingContext, PartTree, JdbcConverter, Dialect, RelationalEntityMetadata, RelationalParameterAccessor, boolean, ReturnedType, Optional, SqlGeneratorSource)}
77+
* {@link JdbcQueryCreator#JdbcQueryCreator(RelationalMappingContext, PartTree, JdbcConverter, Dialect, RelationalEntityMetadata, RelationalParameterAccessor, boolean, ReturnedType, Optional, SqlGeneratorSource, boolean)}
7778
* instead.
7879
*/
7980
@Deprecated(since = "4.0", forRemoval = true)
8081
JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
81-
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
82-
ReturnedType returnedType, Optional<Lock> lockMode) {
82+
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
83+
ReturnedType returnedType, Optional<Lock> lockMode, boolean isScrollQuery) {
8384
this(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType, lockMode,
84-
new SqlGeneratorSource(context, converter, dialect));
85+
new SqlGeneratorSource(context, converter, dialect), isScrollQuery);
8586
}
8687

8788
/**
@@ -96,32 +97,33 @@ public class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery>
9697
* @since 4.0
9798
*/
9899
public JdbcQueryCreator(PartTree tree, JdbcConverter converter, Dialect dialect, JdbcQueryMethod queryMethod,
99-
RelationalParameterAccessor accessor, ReturnedType returnedType) {
100+
RelationalParameterAccessor accessor, ReturnedType returnedType) {
100101
this(converter.getMappingContext(), tree, converter, dialect, queryMethod.getEntityInformation(), accessor,
101102
queryMethod.isSliceQuery(), returnedType, queryMethod.lookupLockAnnotation(),
102-
new SqlGeneratorSource(converter, dialect));
103+
new SqlGeneratorSource(converter, dialect), queryMethod.isScrollQuery());
103104
}
104105

105106
/**
106107
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
107108
* {@link RelationalEntityMetadata} and {@link RelationalParameterAccessor}.
108109
*
109-
* @param context the mapping context. Must not be {@literal null}.
110-
* @param tree part tree, must not be {@literal null}.
111-
* @param converter must not be {@literal null}.
112-
* @param dialect must not be {@literal null}.
113-
* @param entityMetadata relational entity metadata, must not be {@literal null}.
114-
* @param accessor parameter metadata provider, must not be {@literal null}.
115-
* @param isSliceQuery flag denoting if the query returns a {@link org.springframework.data.domain.Slice}.
116-
* @param returnedType the {@link ReturnedType} to be returned by the query. Must not be {@literal null}.
117-
* @param lockMode lock mode to be used for the query.
110+
* @param context the mapping context. Must not be {@literal null}.
111+
* @param tree part tree, must not be {@literal null}.
112+
* @param converter must not be {@literal null}.
113+
* @param dialect must not be {@literal null}.
114+
* @param entityMetadata relational entity metadata, must not be {@literal null}.
115+
* @param accessor parameter metadata provider, must not be {@literal null}.
116+
* @param isSliceQuery flag denoting if the query returns a {@link org.springframework.data.domain.Slice}.
117+
* @param returnedType the {@link ReturnedType} to be returned by the query. Must not be {@literal null}.
118+
* @param lockMode lock mode to be used for the query.
118119
* @param sqlGeneratorSource the source providing SqlGenerator instances for generating SQL. Must not be
119-
* {@literal null}
120+
* {@literal null}
121+
* @param isScrollQuery
120122
* @since 4.0
121123
*/
122124
public JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
123-
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
124-
ReturnedType returnedType, Optional<Lock> lockMode, SqlGeneratorSource sqlGeneratorSource) {
125+
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
126+
ReturnedType returnedType, Optional<Lock> lockMode, SqlGeneratorSource sqlGeneratorSource, boolean isScrollQuery) {
125127
super(tree, accessor);
126128

127129
Assert.notNull(converter, "JdbcConverter must not be null");
@@ -139,6 +141,7 @@ public JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcCon
139141
this.returnedType = returnedType;
140142
this.lockMode = lockMode;
141143
this.statementFactory = new StatementFactory(converter, dialect);
144+
this.isScrollQuery = isScrollQuery;
142145
}
143146

144147
StatementFactory getStatementFactory() {
@@ -205,6 +208,8 @@ protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) {
205208

206209
selection.page(accessor.getPageable()).filter(criteria).orderBy(sort);
207210

211+
selection.scrollPosition(accessor.getScrollPosition());
212+
208213
if (this.lockMode.isPresent()) {
209214
selection.lock(this.lockMode.get().value());
210215
}
@@ -225,6 +230,8 @@ StatementFactory.SelectionBuilder getSelection(RelationalPersistentEntity<?> ent
225230

226231
if (isSliceQuery) {
227232
selection = statementFactory.slice(entity);
233+
} else if (isScrollQuery) {
234+
selection = statementFactory.scroll(entity);
228235
} else {
229236
selection = statementFactory.select(entity);
230237
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,13 @@
2222
import java.util.Collection;
2323
import java.util.List;
2424
import java.util.function.Function;
25+
import java.util.function.IntFunction;
2526
import java.util.function.LongSupplier;
2627
import java.util.function.Supplier;
2728

2829
import org.jspecify.annotations.Nullable;
2930
import org.springframework.core.convert.converter.Converter;
30-
import org.springframework.data.domain.Pageable;
31-
import org.springframework.data.domain.Slice;
32-
import org.springframework.data.domain.SliceImpl;
33-
import org.springframework.data.domain.Sort;
31+
import org.springframework.data.domain.*;
3432
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
3533
import org.springframework.data.jdbc.core.convert.JdbcConverter;
3634
import org.springframework.data.relational.core.conversion.RelationalConverter;
@@ -191,6 +189,10 @@ private JdbcQueryExecution<?> getQueryExecution(ResultProcessor processor,
191189

192190
JdbcQueryExecution<?> queryExecution = getJdbcQueryExecution(extractor, rowMapper);
193191

192+
if (getQueryMethod().isScrollQuery()) {
193+
return new ScrollQueryExecution<>((JdbcQueryExecution<Collection<Object>>) queryExecution, accessor.getScrollPosition(), this.tree.getMaxResults());
194+
}
195+
194196
if (getQueryMethod().isSliceQuery()) {
195197
// noinspection unchecked
196198
return new SliceQueryExecution<>((JdbcQueryExecution<Collection<Object>>) queryExecution, accessor.getPageable());
@@ -205,7 +207,7 @@ private JdbcQueryExecution<?> getQueryExecution(ResultProcessor processor,
205207
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
206208

207209
JdbcCountQueryCreator queryCreator = new JdbcCountQueryCreator(context, tree, converter, dialect,
208-
entityMetadata, accessor, false, processor.getReturnedType(), getQueryMethod().lookupLockAnnotation());
210+
entityMetadata, accessor, false, processor.getReturnedType(), getQueryMethod().lookupLockAnnotation(), false);
209211

210212
ParametrizedQuery countQuery = queryCreator.createQuery(Sort.unsorted());
211213
Object count = singleObjectQuery(new SingleColumnRowMapper<>(Number.class)).execute(countQuery.getQuery(),
@@ -227,7 +229,7 @@ ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor, Re
227229
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
228230

229231
JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor,
230-
getQueryMethod().isSliceQuery(), returnedType, this.getQueryMethod().lookupLockAnnotation());
232+
getQueryMethod().isSliceQuery(), returnedType, this.getQueryMethod().lookupLockAnnotation(), getQueryMethod().isScrollQuery());
231233
return queryCreator.createQuery(getDynamicSort(accessor));
232234
}
233235

@@ -243,7 +245,7 @@ private List<ParametrizedQuery> createDeleteQueries(RelationalParametersParamete
243245
private JdbcQueryExecution<?> getJdbcQueryExecution(@Nullable ResultSetExtractor<Boolean> extractor,
244246
Supplier<RowMapper<?>> rowMapper) {
245247

246-
if (getQueryMethod().isPageQuery() || getQueryMethod().isSliceQuery()) {
248+
if (getQueryMethod().isPageQuery() || getQueryMethod().isSliceQuery() || getQueryMethod().isScrollQuery()) {
247249
return collectionQuery(rowMapper.get());
248250
} else {
249251

@@ -255,6 +257,32 @@ private JdbcQueryExecution<?> getJdbcQueryExecution(@Nullable ResultSetExtractor
255257
}
256258
}
257259

260+
static class ScrollQueryExecution<T> implements JdbcQueryExecution<Window<T>> {
261+
private final JdbcQueryExecution<? extends Collection<T>> delegate;
262+
private final ScrollPosition position;
263+
@Nullable private final Integer maxResults;
264+
265+
ScrollQueryExecution(JdbcQueryExecution<? extends Collection<T>> delegate, ScrollPosition position, @Nullable Integer maxResults) {
266+
this.delegate = delegate;
267+
this.position = position;
268+
this.maxResults = maxResults;
269+
}
270+
271+
@Override
272+
public @Nullable Window<T> execute(String query, SqlParameterSource parameter) {
273+
Collection<T> result = delegate.execute(query, parameter);
274+
275+
List<T> resultList = result instanceof List ? (List<T>) result : new ArrayList<>(result);
276+
IntFunction<? extends ScrollPosition> positionFunction = null;
277+
if (position instanceof OffsetScrollPosition)
278+
positionFunction = ((OffsetScrollPosition) position).positionFunction();
279+
280+
boolean hasNext = resultList.size() >= maxResults;
281+
282+
return Window.from(resultList, positionFunction, hasNext);
283+
}
284+
}
285+
258286
/**
259287
* {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Slice}.
260288
*

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StatementFactory.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323

2424
import org.jspecify.annotations.Nullable;
2525

26-
import org.springframework.data.domain.Limit;
27-
import org.springframework.data.domain.Pageable;
28-
import org.springframework.data.domain.Sort;
26+
import org.springframework.data.domain.*;
2927
import org.springframework.data.jdbc.core.convert.JdbcConverter;
3028
import org.springframework.data.jdbc.core.convert.QueryMapper;
3129
import org.springframework.data.jdbc.core.convert.SqlGeneratorSource;
@@ -103,6 +101,10 @@ public SelectionBuilder slice(RelationalPersistentEntity<?> entity) {
103101
return new SelectionBuilder(entity, SelectionBuilder.Mode.SLICE);
104102
}
105103

104+
public SelectionBuilder scroll(RelationalPersistentEntity<?> entity) {
105+
return new SelectionBuilder(entity, SelectionBuilder.Mode.SELECT);
106+
}
107+
106108
public class SelectionBuilder {
107109

108110
private final RelationalPersistentEntity<?> entity;
@@ -115,13 +117,20 @@ public class SelectionBuilder {
115117
private Sort sort = Sort.unsorted();
116118
private Criteria criteria = Criteria.empty();
117119
private List<String> properties = new ArrayList<>();
120+
private ScrollPosition scrollPosition;
118121

119122
private SelectionBuilder(RelationalPersistentEntity<?> entity, Mode mode) {
120123
this.entity = entity;
121124
this.table = Table.create(entity.getTableName());
122125
this.mode = mode;
123126
}
124127

128+
@Contract("_ -> this")
129+
public SelectionBuilder scrollPosition(ScrollPosition position) {
130+
this.scrollPosition = position;
131+
return this;
132+
}
133+
125134
@Contract("_ -> this")
126135
public SelectionBuilder project(Collection<String> properties) {
127136
this.properties = List.copyOf(properties);
@@ -247,6 +256,11 @@ SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset li
247256
.offset(pageable.getOffset());
248257
}
249258

259+
if (scrollPosition != null && scrollPosition instanceof OffsetScrollPosition && !scrollPosition.isInitial()) {
260+
limitOffsetBuilder = limitOffsetBuilder
261+
.offset(((OffsetScrollPosition) scrollPosition).getOffset() + 1);
262+
}
263+
250264
return (SelectBuilder.SelectWhere) limitOffsetBuilder;
251265
}
252266

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,22 @@ void queryByByteArray() {
14611461
assertThat(result).extracting("idProp").containsExactly(two.idProp);
14621462
}
14631463

1464+
@Test
1465+
void queryByWindowOffset() {
1466+
DummyEntity one = repository.save(createEntity("one"));
1467+
DummyEntity two = repository.save(createEntity("two"));
1468+
DummyEntity three = repository.save(createEntity("three"));
1469+
1470+
WindowIterator<DummyEntity> iter = WindowIterator.of(position -> repository.findFirst2ByOrderByIdPropAsc(position))
1471+
.startingAt(ScrollPosition.offset());
1472+
1473+
List<DummyEntity> entities = new ArrayList<>();
1474+
while (iter.hasNext())
1475+
entities.add(iter.next());
1476+
1477+
assertThat(entities).extracting("idProp").contains(three.idProp);
1478+
}
1479+
14641480
private Root createRoot(String namePrefix) {
14651481

14661482
return new Root(null, namePrefix,
@@ -1606,6 +1622,8 @@ public interface DummyEntityRepository
16061622

16071623
@Query("SELECT * FROM DUMMY_ENTITY WHERE BYTES = :bytes")
16081624
List<DummyEntity> findByBytes(byte[] bytes);
1625+
1626+
Window<DummyEntity> findFirst2ByOrderByIdPropAsc(ScrollPosition position);
16091627
}
16101628

16111629
public interface RootRepository extends ListCrudRepository<Root, Long> {

0 commit comments

Comments
 (0)