1717
1818import static org .springframework .data .jdbc .repository .query .JdbcQueryExecution .*;
1919
20+ import java .lang .reflect .Field ;
2021import java .sql .ResultSet ;
2122import java .util .ArrayList ;
23+ import java .util .Arrays ;
2224import java .util .Collection ;
25+ import java .util .LinkedHashMap ;
2326import java .util .List ;
27+ import java .util .Map ;
2428import java .util .function .Function ;
29+ import java .util .function .IntFunction ;
2530import java .util .function .LongSupplier ;
2631import java .util .function .Supplier ;
32+ import java .util .stream .Collectors ;
2733
2834import org .jspecify .annotations .Nullable ;
2935import org .springframework .core .convert .converter .Converter ;
36+ import org .springframework .data .domain .KeysetScrollPosition ;
37+ import org .springframework .data .domain .Limit ;
38+ import org .springframework .data .domain .OffsetScrollPosition ;
3039import org .springframework .data .domain .Pageable ;
40+ import org .springframework .data .domain .ScrollPosition ;
3141import org .springframework .data .domain .Slice ;
3242import org .springframework .data .domain .SliceImpl ;
3343import org .springframework .data .domain .Sort ;
44+ import org .springframework .data .domain .Window ;
3445import org .springframework .data .jdbc .core .JdbcAggregateOperations ;
3546import org .springframework .data .jdbc .core .convert .JdbcConverter ;
3647import org .springframework .data .relational .core .conversion .RelationalConverter ;
3748import org .springframework .data .relational .core .dialect .Dialect ;
3849import org .springframework .data .relational .core .mapping .RelationalMappingContext ;
50+ import org .springframework .data .relational .core .mapping .RelationalPersistentEntity ;
51+ import org .springframework .data .relational .core .mapping .RelationalPersistentProperty ;
3952import org .springframework .data .relational .repository .query .RelationalEntityMetadata ;
4053import org .springframework .data .relational .repository .query .RelationalParameterAccessor ;
4154import org .springframework .data .relational .repository .query .RelationalParametersParameterAccessor ;
5164import org .springframework .jdbc .core .namedparam .NamedParameterJdbcOperations ;
5265import org .springframework .jdbc .core .namedparam .SqlParameterSource ;
5366import org .springframework .util .Assert ;
67+ import org .springframework .util .ReflectionUtils ;
5468
5569/**
5670 * An {@link AbstractJdbcQuery} implementation based on a {@link PartTree}.
6175 * @author Mikhail Polivakha
6276 * @author Yunyoung LEE
6377 * @author Nikita Konev
78+ * @author Artemij Degtyarev
6479 * @since 2.0
6580 */
6681public class PartTreeJdbcQuery extends AbstractJdbcQuery {
@@ -191,6 +206,13 @@ private JdbcQueryExecution<?> getQueryExecution(ResultProcessor processor,
191206
192207 JdbcQueryExecution <?> queryExecution = getJdbcQueryExecution (extractor , rowMapper );
193208
209+ if (getQueryMethod ().isScrollQuery ()) {
210+ // noinspection unchecked
211+ return new ScrollQueryExecution <>((JdbcQueryExecution <Collection <Object >>) queryExecution ,
212+ accessor .getScrollPosition (), this .tree .getMaxResults (), tree .getSort (), tree .getResultLimit (),
213+ getQueryMethod ().getEntityInformation ().getTableEntity ());
214+ }
215+
194216 if (getQueryMethod ().isSliceQuery ()) {
195217 // noinspection unchecked
196218 return new SliceQueryExecution <>((JdbcQueryExecution <Collection <Object >>) queryExecution , accessor .getPageable ());
@@ -205,7 +227,8 @@ private JdbcQueryExecution<?> getQueryExecution(ResultProcessor processor,
205227 RelationalEntityMetadata <?> entityMetadata = getQueryMethod ().getEntityInformation ();
206228
207229 JdbcCountQueryCreator queryCreator = new JdbcCountQueryCreator (context , tree , converter , dialect ,
208- entityMetadata , accessor , false , processor .getReturnedType (), getQueryMethod ().lookupLockAnnotation ());
230+ entityMetadata , accessor , false , processor .getReturnedType (), getQueryMethod ().lookupLockAnnotation (),
231+ false );
209232
210233 ParametrizedQuery countQuery = queryCreator .createQuery (Sort .unsorted ());
211234 Object count = singleObjectQuery (new SingleColumnRowMapper <>(Number .class )).execute (countQuery .getQuery (),
@@ -227,7 +250,8 @@ ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor, Re
227250 RelationalEntityMetadata <?> entityMetadata = getQueryMethod ().getEntityInformation ();
228251
229252 JdbcQueryCreator queryCreator = new JdbcQueryCreator (context , tree , converter , dialect , entityMetadata , accessor ,
230- getQueryMethod ().isSliceQuery (), returnedType , this .getQueryMethod ().lookupLockAnnotation ());
253+ getQueryMethod ().isSliceQuery (), returnedType , this .getQueryMethod ().lookupLockAnnotation (),
254+ getQueryMethod ().isScrollQuery ());
231255 return queryCreator .createQuery (getDynamicSort (accessor ));
232256 }
233257
@@ -243,7 +267,7 @@ private List<ParametrizedQuery> createDeleteQueries(RelationalParametersParamete
243267 private JdbcQueryExecution <?> getJdbcQueryExecution (@ Nullable ResultSetExtractor <Boolean > extractor ,
244268 Supplier <RowMapper <?>> rowMapper ) {
245269
246- if (getQueryMethod ().isPageQuery () || getQueryMethod ().isSliceQuery ()) {
270+ if (getQueryMethod ().isPageQuery () || getQueryMethod ().isSliceQuery () || getQueryMethod (). isScrollQuery () ) {
247271 return collectionQuery (rowMapper .get ());
248272 } else {
249273
@@ -255,6 +279,97 @@ private JdbcQueryExecution<?> getJdbcQueryExecution(@Nullable ResultSetExtractor
255279 }
256280 }
257281
282+ /**
283+ * {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Window}
284+ *
285+ * @param <T>
286+ */
287+ static class ScrollQueryExecution <T > implements JdbcQueryExecution <Window <T >> {
288+ private final JdbcQueryExecution <? extends Collection <T >> delegate ;
289+ private final @ Nullable ScrollPosition position ;
290+ private final @ Nullable Integer maxResults ;
291+ private final Sort sort ;
292+ private final Limit limit ;
293+ private final RelationalPersistentEntity <?> tableEntity ;
294+
295+ ScrollQueryExecution (JdbcQueryExecution <? extends Collection <T >> delegate , @ Nullable ScrollPosition position ,
296+ @ Nullable Integer maxResults , Sort sort , Limit limit , RelationalPersistentEntity <?> tableEntity ) {
297+ this .delegate = delegate ;
298+ this .position = position ;
299+ this .maxResults = maxResults ;
300+ this .sort = sort ;
301+ this .limit = limit ;
302+ this .tableEntity = tableEntity ;
303+ }
304+
305+ @ Override
306+ public @ Nullable Window <T > execute (String query , SqlParameterSource parameter ) {
307+ Collection <T > result = delegate .execute (query , parameter );
308+
309+ List <T > resultList = result instanceof List ? (List <T >) result : new ArrayList <>(result );
310+ IntFunction <? extends ScrollPosition > positionFunction = null ;
311+ if (position instanceof OffsetScrollPosition )
312+ positionFunction = ((OffsetScrollPosition ) position ).positionFunction ();
313+
314+ if (position instanceof KeysetScrollPosition ) {
315+ Map <String , Object > keys = ((KeysetScrollPosition ) position ).getKeys ();
316+ List <String > orders = new ArrayList <>(keys .keySet ());
317+
318+ if (orders .isEmpty ())
319+ orders = sort .get ().map (Sort .Order ::getProperty ).toList ();
320+
321+ orders = orders .stream ().map (it -> {
322+ RelationalPersistentProperty prop = tableEntity .getPersistentProperty (it );
323+
324+ if (prop == null )
325+ return it ;
326+
327+ return prop .getName ();
328+ }).toList ();
329+
330+ keys = extractKeys (resultList , orders );
331+
332+ Map <String , Object > finalKeys = keys ;
333+ positionFunction = (ignoredI ) -> ScrollPosition .of (finalKeys , ((KeysetScrollPosition ) position ).getDirection ());
334+ }
335+
336+ if (positionFunction == null )
337+ throw new UnsupportedOperationException ("Not supported scroll type." );
338+
339+ boolean hasNext ;
340+ if (maxResults != null )
341+ hasNext = resultList .size () >= maxResults ;
342+ else if (limit .isLimited ())
343+ hasNext = resultList .size () >= limit .max ();
344+ else
345+ hasNext = !resultList .isEmpty ();
346+
347+ return Window .from (resultList , positionFunction , hasNext );
348+ }
349+
350+ private Map <String , Object > extractKeys (List <T > resultList , List <String > orders ) {
351+ if (resultList .isEmpty ())
352+ return Map .of ();
353+
354+ T last = resultList .get (resultList .size () - 1 );
355+
356+ Field [] fields = last .getClass ().getDeclaredFields ();
357+
358+ // noinspection DataFlowIssue
359+ return Arrays .stream (fields ).filter (it -> {
360+ String name = it .getName ();
361+
362+ RelationalPersistentProperty prop = tableEntity .getPersistentProperty (name );
363+ if (prop != null )
364+ name = prop .getName ();
365+
366+ String finalName = name ;
367+ return orders .stream ().anyMatch (order -> order .equalsIgnoreCase (finalName ));
368+ }).peek (ReflectionUtils ::makeAccessible ).collect (Collectors .toMap (Field ::getName ,
369+ it -> ReflectionUtils .getField (it , last ), (e1 , e2 ) -> e1 , LinkedHashMap ::new ));
370+ }
371+ }
372+
258373 /**
259374 * {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Slice}.
260375 *
@@ -327,8 +442,7 @@ class CachedRowMapperFactory implements Supplier<RowMapper<?>> {
327442 private final Lazy <RowMapper <?>> rowMapper ;
328443 private final Function <ResultProcessor , RowMapper <?>> rowMapperFunction ;
329444
330- public CachedRowMapperFactory (PartTree tree ,
331- RowMapperFactory rowMapperFactory , RelationalConverter converter ,
445+ public CachedRowMapperFactory (PartTree tree , RowMapperFactory rowMapperFactory , RelationalConverter converter ,
332446 ResultProcessor defaultResultProcessor ) {
333447
334448 this .rowMapperFunction = processor -> {
@@ -338,8 +452,8 @@ public CachedRowMapperFactory(PartTree tree,
338452 }
339453 Converter <Object , Object > resultProcessingConverter = new ResultProcessingConverter (processor ,
340454 converter .getMappingContext (), converter .getEntityInstantiators ());
341- return new ConvertingRowMapper (
342- rowMapperFactory . create ( processor . getReturnedType (). getDomainType ()), resultProcessingConverter );
455+ return new ConvertingRowMapper (rowMapperFactory . create ( processor . getReturnedType (). getDomainType ()),
456+ resultProcessingConverter );
343457 };
344458
345459 this .rowMapper = Lazy .of (() -> this .rowMapperFunction .apply (defaultResultProcessor ));
0 commit comments