Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import java.util.stream.Stream;

import org.jspecify.annotations.Nullable;

import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.domain.Sort;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
*
* @author Mark Paluch
* @author Diego Krupitza
* @author Artemiy Degtyarev
* @since 2.2
*/
public class JdbcCountQueryCreator extends JdbcQueryCreator {
Expand All @@ -44,8 +45,9 @@ public JdbcCountQueryCreator(PartTree tree, JdbcConverter converter, Dialect dia

JdbcCountQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
ReturnedType returnedType, Optional<Lock> lockMode) {
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType, lockMode);
ReturnedType returnedType, Optional<Lock> lockMode, boolean isScrollQuery) {
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType, lockMode,
isScrollQuery);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import java.util.Optional;

import org.jspecify.annotations.Nullable;

import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.SqlGeneratorSource;
Expand Down Expand Up @@ -47,6 +46,7 @@
* @author Jens Schauder
* @author Myeonghyeon Lee
* @author Diego Krupitza
* @author Artemiy Degtyarev
* @since 2.0
*/
public class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
Expand All @@ -59,6 +59,7 @@ public class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery>
private final ReturnedType returnedType;
private final Optional<Lock> lockMode;
private final StatementFactory statementFactory;
private final boolean isScrollQuery;

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

/**
Expand All @@ -99,7 +100,7 @@ public JdbcQueryCreator(PartTree tree, JdbcConverter converter, Dialect dialect,
RelationalParameterAccessor accessor, ReturnedType returnedType) {
this(converter.getMappingContext(), tree, converter, dialect, queryMethod.getEntityInformation(), accessor,
queryMethod.isSliceQuery(), returnedType, queryMethod.lookupLockAnnotation(),
new SqlGeneratorSource(converter, dialect));
new SqlGeneratorSource(converter, dialect), queryMethod.isScrollQuery());
}

/**
Expand All @@ -117,11 +118,13 @@ public JdbcQueryCreator(PartTree tree, JdbcConverter converter, Dialect dialect,
* @param lockMode lock mode to be used for the query.
* @param sqlGeneratorSource the source providing SqlGenerator instances for generating SQL. Must not be
* {@literal null}
* @param isScrollQuery flag denoting if the query returns a {@link org.springframework.data.domain.Window}.
* @since 4.0
*/
public JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
ReturnedType returnedType, Optional<Lock> lockMode, SqlGeneratorSource sqlGeneratorSource) {
ReturnedType returnedType, Optional<Lock> lockMode, SqlGeneratorSource sqlGeneratorSource,
boolean isScrollQuery) {
super(tree, accessor);

Assert.notNull(converter, "JdbcConverter must not be null");
Expand All @@ -139,6 +142,7 @@ public JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcCon
this.returnedType = returnedType;
this.lockMode = lockMode;
this.statementFactory = new StatementFactory(converter, dialect);
this.isScrollQuery = isScrollQuery;
}

StatementFactory getStatementFactory() {
Expand Down Expand Up @@ -205,6 +209,8 @@ protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) {

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

selection.scrollPosition(accessor.getScrollPosition());

if (this.lockMode.isPresent()) {
selection.lock(this.lockMode.get().value());
}
Expand All @@ -225,6 +231,8 @@ StatementFactory.SelectionBuilder getSelection(RelationalPersistentEntity<?> ent

if (isSliceQuery) {
selection = statementFactory.slice(entity);
} else if (isScrollQuery) {
selection = statementFactory.scroll(entity);
} else {
selection = statementFactory.select(entity);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,33 @@
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.LongSupplier;
import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
Expand All @@ -61,6 +72,7 @@
* @author Mikhail Polivakha
* @author Yunyoung LEE
* @author Nikita Konev
* @author Artemij Degtyarev
* @since 2.0
*/
public class PartTreeJdbcQuery extends AbstractJdbcQuery {
Expand Down Expand Up @@ -191,6 +203,13 @@ private JdbcQueryExecution<?> getQueryExecution(ResultProcessor processor,

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

if (getQueryMethod().isScrollQuery()) {
// noinspection unchecked
return new ScrollQueryExecution<>((JdbcQueryExecution<Collection<Object>>) queryExecution,
accessor.getScrollPosition(), this.tree.getMaxResults(), tree.getSort(), tree.getResultLimit(),
getQueryMethod().getEntityInformation().getTableEntity());
}

if (getQueryMethod().isSliceQuery()) {
// noinspection unchecked
return new SliceQueryExecution<>((JdbcQueryExecution<Collection<Object>>) queryExecution, accessor.getPageable());
Expand All @@ -205,7 +224,8 @@ private JdbcQueryExecution<?> getQueryExecution(ResultProcessor processor,
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();

JdbcCountQueryCreator queryCreator = new JdbcCountQueryCreator(context, tree, converter, dialect,
entityMetadata, accessor, false, processor.getReturnedType(), getQueryMethod().lookupLockAnnotation());
entityMetadata, accessor, false, processor.getReturnedType(), getQueryMethod().lookupLockAnnotation(),
false);

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

JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor,
getQueryMethod().isSliceQuery(), returnedType, this.getQueryMethod().lookupLockAnnotation());
getQueryMethod().isSliceQuery(), returnedType, this.getQueryMethod().lookupLockAnnotation(),
getQueryMethod().isScrollQuery());
return queryCreator.createQuery(getDynamicSort(accessor));
}

Expand All @@ -243,7 +264,7 @@ private List<ParametrizedQuery> createDeleteQueries(RelationalParametersParamete
private JdbcQueryExecution<?> getJdbcQueryExecution(@Nullable ResultSetExtractor<Boolean> extractor,
Supplier<RowMapper<?>> rowMapper) {

if (getQueryMethod().isPageQuery() || getQueryMethod().isSliceQuery()) {
if (getQueryMethod().isPageQuery() || getQueryMethod().isSliceQuery() || getQueryMethod().isScrollQuery()) {
return collectionQuery(rowMapper.get());
} else {

Expand All @@ -255,6 +276,95 @@ private JdbcQueryExecution<?> getJdbcQueryExecution(@Nullable ResultSetExtractor
}
}

/**
* {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Window}
*
* @param <T>
*/
static class ScrollQueryExecution<T> implements JdbcQueryExecution<Window<T>> {
private final JdbcQueryExecution<? extends Collection<T>> delegate;
private final @Nullable ScrollPosition position;
private final @Nullable Integer maxResults;
private final Sort sort;
private final Limit limit;
private final RelationalPersistentEntity<?> tableEntity;

ScrollQueryExecution(JdbcQueryExecution<? extends Collection<T>> delegate, @Nullable ScrollPosition position,
@Nullable Integer maxResults, Sort sort, Limit limit, RelationalPersistentEntity<?> tableEntity) {
this.delegate = delegate;
this.position = position;
this.maxResults = maxResults;
this.sort = sort;
this.limit = limit;
this.tableEntity = tableEntity;
}

@Override
public @Nullable Window<T> execute(String query, SqlParameterSource parameter) {
Collection<T> result = delegate.execute(query, parameter);

List<T> resultList = result instanceof List ? (List<T>) result : new ArrayList<>(result);
IntFunction<? extends ScrollPosition> positionFunction = null;
if (position instanceof OffsetScrollPosition)
positionFunction = ((OffsetScrollPosition) position).positionFunction();

if (position instanceof KeysetScrollPosition) {
Map<String, Object> keys = ((KeysetScrollPosition) position).getKeys();
List<String> orders = new ArrayList<>(keys.keySet());

if (orders.isEmpty())
orders = sort.get().map(Sort.Order::getProperty).toList();

List<RelationalPersistentProperty> properties = new ArrayList<>();
for (String propertyName : orders) {
RelationalPersistentProperty prop = tableEntity.getPersistentProperty(propertyName);
if (prop == null)
continue;

properties.add(prop);
}

final Map<String, Object> resultKeys = extractKeys(resultList, properties);
positionFunction = (ignoredI) -> ScrollPosition.of(resultKeys, ((KeysetScrollPosition) position).getDirection());
}

if (positionFunction == null)
throw new UnsupportedOperationException("Not supported scroll type.");

boolean hasNext;
if (maxResults != null)
hasNext = resultList.size() >= maxResults;
else if (limit.isLimited())
hasNext = resultList.size() >= limit.max();
else
hasNext = !resultList.isEmpty();

return Window.from(resultList, positionFunction, hasNext);
}

private Map<String, Object> extractKeys(List<T> resultList, List<RelationalPersistentProperty> properties) {
if (resultList.isEmpty())
return Map.of();

Map<String, Object> result = new LinkedHashMap<>();

T last = resultList.get(resultList.size() - 1);
PersistentPropertyAccessor<T> accessor = tableEntity.getPropertyAccessor(last);

for (RelationalPersistentProperty property : properties) {
String propertyName = property.getName();
Object propertyValue = accessor.getProperty(property);

if (propertyValue == null)
continue;

result.put(propertyName, propertyValue);
}

return result;
}
}

/**
* {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Slice}.
*
Expand Down Expand Up @@ -327,8 +437,7 @@ class CachedRowMapperFactory implements Supplier<RowMapper<?>> {
private final Lazy<RowMapper<?>> rowMapper;
private final Function<ResultProcessor, RowMapper<?>> rowMapperFunction;

public CachedRowMapperFactory(PartTree tree,
RowMapperFactory rowMapperFactory, RelationalConverter converter,
public CachedRowMapperFactory(PartTree tree, RowMapperFactory rowMapperFactory, RelationalConverter converter,
ResultProcessor defaultResultProcessor) {

this.rowMapperFunction = processor -> {
Expand All @@ -338,8 +447,8 @@ public CachedRowMapperFactory(PartTree tree,
}
Converter<Object, Object> resultProcessingConverter = new ResultProcessingConverter(processor,
converter.getMappingContext(), converter.getEntityInstantiators());
return new ConvertingRowMapper(
rowMapperFactory.create(processor.getReturnedType().getDomainType()), resultProcessingConverter);
return new ConvertingRowMapper(rowMapperFactory.create(processor.getReturnedType().getDomainType()),
resultProcessingConverter);
};

this.rowMapper = Lazy.of(() -> this.rowMapperFunction.apply(defaultResultProcessor));
Expand Down
Loading