keysetValues = keyset.getKeys();
+
+ // first query doesn't come with a keyset
+ if (keysetValues.isEmpty()) {
+ return null;
+ }
+
+ List or = new ArrayList<>();
+ int i = 0;
+
+ // progressive query building to reconstruct a query matching sorting rules
+ for (Order order : sort) {
+
+ if (!keysetValues.containsKey(order.getProperty())) {
+ throw new IllegalStateException(String
+ .format("KeysetScrollPosition does not contain all keyset values. Missing key: %s", order.getProperty()));
+ }
+
+ List
sortConstraint = new ArrayList<>();
+
+ int j = 0;
+ for (Order inner : sort) {
+
+ E propertyExpression = strategy.createExpression(inner.getProperty());
+ Object o = keysetValues.get(inner.getProperty());
+
+ if (j >= i) { // tail segment
+
+ sortConstraint.add(strategy.compare(inner, propertyExpression, o));
+ break;
+ }
+
+ sortConstraint.add(strategy.compare(propertyExpression, o));
+ j++;
+ }
+
+ if (!sortConstraint.isEmpty()) {
+ or.add(strategy.and(sortConstraint));
+ }
+
+ i++;
+ }
+
+ if (or.isEmpty()) {
+ return null;
+ }
+
+ return strategy.or(or);
+ }
+
+ protected Sort getSortOrders(Sort sort) {
+ return sort;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected List postProcessResults(List result) {
+ return result;
+ }
+
+ protected List getResultWindow(List list, int limit) {
+ return CollectionUtils.getFirst(limit, list);
+ }
+
+ /**
+ * Reverse scrolling variant applying {@link Direction#Backward}. In reverse scrolling, we need to flip directions for
+ * the actual query so that we do not get everything from the top position and apply the limit but rather flip the
+ * sort direction, apply the limit and then reverse the result to restore the actual sort order.
+ */
+ private static class ReverseKeysetScrollDelegate extends KeysetScrollDelegate {
+
+ protected Sort getSortOrders(Sort sort) {
+
+ List orders = new ArrayList<>();
+ for (Order order : sort) {
+ orders.add(new Order(order.isAscending() ? Sort.Direction.DESC : Sort.Direction.ASC, order.getProperty()));
+ }
+
+ return Sort.by(orders);
+ }
+
+ @Override
+ protected List postProcessResults(List result) {
+ Collections.reverse(result);
+ return result;
+ }
+
+ @Override
+ protected List getResultWindow(List list, int limit) {
+ return CollectionUtils.getLast(limit, list);
+ }
+ }
+
+ /**
+ * Adapter to construct scroll queries.
+ *
+ * @param property path expression type.
+ * @param predicate type.
+ */
+ public interface QueryStrategy {
+
+ /**
+ * Create an expression object from the given {@code property} path.
+ *
+ * @param property must not be {@literal null}.
+ * @return
+ */
+ E createExpression(String property);
+
+ /**
+ * Create a comparison object according to the {@link Order}.
+ *
+ * @param order must not be {@literal null}.
+ * @param propertyExpression must not be {@literal null}.
+ * @param value
+ * @return
+ */
+ P compare(Order order, E propertyExpression, Object value);
+
+ /**
+ * Create an equals-comparison object.
+ *
+ * @param propertyExpression must not be {@literal null}.
+ * @param value
+ * @return
+ */
+ P compare(E propertyExpression, @Nullable Object value);
+
+ /**
+ * AND-combine the {@code intermediate} predicates.
+ *
+ * @param intermediate
+ * @return
+ */
+ P and(List intermediate);
+
+ /**
+ * OR-combine the {@code intermediate} predicates.
+ *
+ * @param intermediate
+ * @return
+ */
+ P or(List
intermediate);
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java
new file mode 100644
index 0000000000..cfec4f06df
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.jpa.repository.query;
+
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Expression;
+import jakarta.persistence.criteria.From;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import java.util.List;
+
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.repository.query.KeysetScrollDelegate.QueryStrategy;
+import org.springframework.data.jpa.repository.support.JpaEntityInformation;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.lang.Nullable;
+
+/**
+ * {@link Specification} to create scroll queries using keyset-scrolling.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @since 3.1
+ */
+public record KeysetScrollSpecification (KeysetScrollPosition position, Sort sort,
+ JpaEntityInformation, ?> entity) implements Specification {
+
+ public KeysetScrollSpecification(KeysetScrollPosition position, Sort sort, JpaEntityInformation, ?> entity) {
+
+ this.position = position;
+ this.entity = entity;
+ this.sort = createSort(position, sort, entity);
+ }
+
+ /**
+ * Create a {@link Sort} object to be used with the actual query.
+ *
+ * @param position must not be {@literal null}.
+ * @param sort must not be {@literal null}.
+ * @param entity must not be {@literal null}.
+ * @return
+ */
+ public static Sort createSort(KeysetScrollPosition position, Sort sort, JpaEntityInformation, ?> entity) {
+
+ KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection());
+
+ Sort sortToUse;
+ if (entity.hasCompositeId()) {
+ sortToUse = sort.and(Sort.by(entity.getIdAttributeNames().toArray(new String[0])));
+ } else {
+ sortToUse = sort.and(Sort.by(entity.getRequiredIdAttribute().getName()));
+ }
+
+ return delegate.getSortOrders(sortToUse);
+ }
+
+ @Override
+ public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder criteriaBuilder) {
+ return createPredicate(root, criteriaBuilder);
+ }
+
+ @Nullable
+ public Predicate createPredicate(Root> root, CriteriaBuilder criteriaBuilder) {
+
+ KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection());
+ return delegate.createPredicate(position, sort, new JpaQueryStrategy(root, criteriaBuilder));
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static class JpaQueryStrategy implements QueryStrategy, Predicate> {
+
+ private final From, ?> from;
+ private final CriteriaBuilder cb;
+
+ public JpaQueryStrategy(From, ?> from, CriteriaBuilder cb) {
+
+ this.from = from;
+ this.cb = cb;
+ }
+
+ @Override
+ public Expression createExpression(String property) {
+
+ PropertyPath path = PropertyPath.from(property, from.getJavaType());
+ return QueryUtils.toExpressionRecursively(from, path);
+ }
+
+ @Override
+ public Predicate compare(Order order, Expression propertyExpression, Object value) {
+
+ return order.isAscending() ? cb.greaterThan(propertyExpression, (Comparable) value)
+ : cb.lessThan(propertyExpression, (Comparable) value);
+ }
+
+ @Override
+ public Predicate compare(Expression propertyExpression, @Nullable Object value) {
+ return value == null ? cb.isNull(propertyExpression) : cb.equal(propertyExpression, value);
+ }
+
+ @Override
+ public Predicate and(List intermediate) {
+ return cb.and(intermediate.toArray(new Predicate[0]));
+ }
+
+ @Override
+ public Predicate or(List intermediate) {
+ return cb.or(intermediate.toArray(new Predicate[0]));
+ }
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
index 45442a4c49..311f47eafd 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
@@ -145,6 +145,10 @@ public static RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em
return null;
}
+ if (method.isScrollQuery()) {
+ throw QueryCreationException.create(method, "Scroll queries are not supported using String-based queries");
+ }
+
try {
RepositoryQuery query = new NamedQuery(method, em);
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java
index 8bfbb72a49..78cfde34a5 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java
@@ -15,19 +15,25 @@
*/
package org.springframework.data.jpa.repository.query;
-import java.util.List;
-
import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceUnitUtil;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
+import java.util.List;
+
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.OffsetScrollPosition;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.DeleteExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ExistsExecution;
+import org.springframework.data.jpa.repository.query.JpaQueryExecution.ScrollExecution;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
+import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.Part;
@@ -55,6 +61,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
private final QueryPreparer countQuery;
private final EntityManager em;
private final EscapeCharacter escape;
+ private final JpaMetamodelEntityInformation, Object> entityInformation;
/**
* Creates a new {@link PartTreeJpaQuery}.
@@ -79,10 +86,14 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
this.em = em;
this.escape = escape;
- Class> domainClass = method.getEntityInformation().getJavaType();
this.parameters = method.getParameters();
- boolean recreationRequired = parameters.hasDynamicProjection() || parameters.potentiallySortsDynamically();
+ Class> domainClass = method.getEntityInformation().getJavaType();
+ PersistenceUnitUtil persistenceUnitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil();
+ this.entityInformation = new JpaMetamodelEntityInformation<>(domainClass, em.getMetamodel(), persistenceUnitUtil);
+
+ boolean recreationRequired = parameters.hasDynamicProjection() || parameters.potentiallySortsDynamically()
+ || method.isScrollQuery();
try {
@@ -111,7 +122,9 @@ public TypedQuery doCreateCountQuery(JpaParametersParameterAccessor access
@Override
protected JpaQueryExecution getExecution() {
- if (this.tree.isDelete()) {
+ if (this.getQueryMethod().isScrollQuery()) {
+ return new ScrollExecution(this.tree.getSort(), new ScrollDelegate<>(entityInformation));
+ } else if (this.tree.isDelete()) {
return new DeleteExecution(em);
} else if (this.tree.isExistsProjection()) {
return new ExistsExecution();
@@ -228,7 +241,11 @@ public Query createQuery(JpaParametersParameterAccessor accessor) {
TypedQuery> query = createQuery(criteriaQuery);
- return restrictMaxResultsIfNecessary(invokeBinding(parameterBinder, query, accessor, this.metadataCache));
+ ScrollPosition scrollPosition = accessor.getParameters().hasScrollPositionParameter()
+ ? accessor.getScrollPosition()
+ : null;
+ return restrictMaxResultsIfNecessary(invokeBinding(parameterBinder, query, accessor, this.metadataCache),
+ scrollPosition);
}
/**
@@ -236,10 +253,14 @@ public Query createQuery(JpaParametersParameterAccessor accessor) {
* limited.
*/
@SuppressWarnings("ConstantConditions")
- private Query restrictMaxResultsIfNecessary(Query query) {
+ private Query restrictMaxResultsIfNecessary(Query query, @Nullable ScrollPosition scrollPosition) {
if (tree.isLimiting()) {
+ if (scrollPosition instanceof OffsetScrollPosition offset) {
+ query.setFirstResult(Math.toIntExact(offset.getOffset()));
+ }
+
if (query.getMaxResults() != Integer.MAX_VALUE) {
/*
* In order to return the correct results, we have to adjust the first result offset to be returned if:
@@ -298,6 +319,10 @@ protected JpaQueryCreator createCreator(@Nullable JpaParametersParameterAccessor
returnedType = processor.getReturnedType();
}
+ if (accessor != null && accessor.getScrollPosition()instanceof KeysetScrollPosition keyset) {
+ return new JpaKeysetScrollQueryCreator(tree, returnedType, builder, provider, entityInformation, keyset);
+ }
+
return new JpaQueryCreator(tree, returnedType, builder, provider);
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ScrollDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ScrollDelegate.java
new file mode 100644
index 0000000000..94cc960a9b
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ScrollDelegate.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.jpa.repository.query;
+
+import jakarta.persistence.Query;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.IntFunction;
+
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.KeysetScrollPosition.Direction;
+import org.springframework.data.domain.OffsetScrollPosition;
+import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.domain.Window;
+import org.springframework.data.jpa.repository.support.JpaEntityInformation;
+import org.springframework.util.Assert;
+
+/**
+ * Delegate to run {@link ScrollPosition scroll queries} and create result {@link Window}.
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ */
+public class ScrollDelegate {
+
+ private final JpaEntityInformation entity;
+
+ protected ScrollDelegate(JpaEntityInformation entity) {
+ this.entity = entity;
+ }
+
+ /**
+ * Run the {@link Query} and return a scroll {@link Window}.
+ *
+ * @param query must not be {@literal null}.
+ * @param sort must not be {@literal null}.
+ * @param scrollPosition must not be {@literal null}.
+ * @return the scroll {@link Window}.
+ */
+ @SuppressWarnings("unchecked")
+ public Window scroll(Query query, Sort sort, ScrollPosition scrollPosition) {
+
+ Assert.notNull(scrollPosition, "ScrollPosition must not be null");
+
+ int limit = query.getMaxResults();
+ if (limit > 0 && limit != Integer.MAX_VALUE) {
+ query = query.setMaxResults(limit + 1);
+ }
+
+ List result = query.getResultList();
+
+ if (scrollPosition instanceof KeysetScrollPosition keyset) {
+ return createWindow(sort, limit, keyset.getDirection(), entity, result);
+ }
+
+ if (scrollPosition instanceof OffsetScrollPosition offset) {
+ return createWindow(result, limit, OffsetScrollPosition.positionFunction(offset.getOffset()));
+ }
+
+ throw new UnsupportedOperationException("ScrollPosition " + scrollPosition + " not supported");
+ }
+
+ private static Window createWindow(Sort sort, int limit, Direction direction,
+ JpaEntityInformation entity, List result) {
+
+ KeysetScrollDelegate delegate = KeysetScrollDelegate.of(direction);
+ List resultsToUse = delegate.postProcessResults(result);
+
+ IntFunction positionFunction = value -> {
+
+ T object = result.get(value);
+ Map keys = entity.getKeyset(sort.stream().map(Order::getProperty).toList(), object);
+
+ return KeysetScrollPosition.of(keys);
+ };
+
+ return Window.from(delegate.getResultWindow(resultsToUse, limit), positionFunction, hasMoreElements(result, limit));
+ }
+
+ private static Window createWindow(List result, int limit,
+ IntFunction extends ScrollPosition> positionFunction) {
+ return Window.from(CollectionUtils.getFirst(limit, result), positionFunction, hasMoreElements(result, limit));
+ }
+
+ private static boolean hasMoreElements(List> result, int limit) {
+ return !result.isEmpty() && result.size() > limit;
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java
deleted file mode 100644
index d1ddea66ba..0000000000
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright 2021-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.data.jpa.repository.support;
-
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.TypedQuery;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Function;
-import java.util.stream.Stream;
-
-import org.springframework.dao.IncorrectResultSizeDataAccessException;
-import org.springframework.data.domain.Example;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.PageImpl;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Sort;
-import org.springframework.data.jpa.repository.query.EscapeCharacter;
-import org.springframework.data.jpa.support.PageableUtils;
-import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
-import org.springframework.data.support.PageableExecutionUtils;
-import org.springframework.util.Assert;
-
-/**
- * Immutable implementation of {@link FetchableFluentQuery} based on Query by {@link Example}. All methods that return a
- * {@link FetchableFluentQuery} will return a new instance, not the original.
- *
- * @param Domain type
- * @param Result type
- * @author Greg Turnquist
- * @author Mark Paluch
- * @author Jens Schauder
- * @author J.R. Onyschak
- * @since 2.6
- */
-class FetchableFluentQueryByExample extends FluentQuerySupport implements FetchableFluentQuery {
-
- private final Example example;
- private final Function> finder;
- private final Function, Long> countOperation;
- private final Function, Boolean> existsOperation;
- private final EntityManager entityManager;
- private final EscapeCharacter escapeCharacter;
-
- public FetchableFluentQueryByExample(Example example, Function> finder,
- Function, Long> countOperation, Function, Boolean> existsOperation,
- EntityManager entityManager, EscapeCharacter escapeCharacter) {
- this(example, example.getProbeType(), (Class) example.getProbeType(), Sort.unsorted(), Collections.emptySet(),
- finder, countOperation, existsOperation, entityManager, escapeCharacter);
- }
-
- private FetchableFluentQueryByExample(Example example, Class entityType, Class returnType, Sort sort,
- Collection properties, Function> finder, Function, Long> countOperation,
- Function, Boolean> existsOperation, EntityManager entityManager, EscapeCharacter escapeCharacter) {
-
- super(returnType, sort, properties, entityType);
- this.example = example;
- this.finder = finder;
- this.countOperation = countOperation;
- this.existsOperation = existsOperation;
- this.entityManager = entityManager;
- this.escapeCharacter = escapeCharacter;
- }
-
- @Override
- public FetchableFluentQuery sortBy(Sort sort) {
-
- Assert.notNull(sort, "Sort must not be null");
-
- return new FetchableFluentQueryByExample<>(example, entityType, resultType, this.sort.and(sort), properties, finder,
- countOperation, existsOperation, entityManager, escapeCharacter);
- }
-
- @Override
- public FetchableFluentQuery as(Class resultType) {
-
- Assert.notNull(resultType, "Projection target type must not be null");
- if (!resultType.isInterface()) {
- throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
- }
-
- return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, properties, finder,
- countOperation, existsOperation, entityManager, escapeCharacter);
- }
-
- @Override
- public FetchableFluentQuery project(Collection properties) {
-
- return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, mergeProperties(properties),
- finder, countOperation, existsOperation, entityManager, escapeCharacter);
- }
-
- @Override
- public R oneValue() {
-
- TypedQuery limitedQuery = createSortedAndProjectedQuery();
- limitedQuery.setMaxResults(2); // Never need more than 2 values
-
- List results = limitedQuery.getResultList();
-
- if (results.size() > 1) {
- throw new IncorrectResultSizeDataAccessException(1);
- }
-
- return results.isEmpty() ? null : getConversionFunction().apply(results.get(0));
- }
-
- @Override
- public R firstValue() {
-
- TypedQuery limitedQuery = createSortedAndProjectedQuery();
- limitedQuery.setMaxResults(1); // Never need more than 1 value
-
- List results = limitedQuery.getResultList();
-
- return results.isEmpty() ? null : getConversionFunction().apply(results.get(0));
- }
-
- @Override
- public List all() {
-
- List resultList = createSortedAndProjectedQuery().getResultList();
-
- return convert(resultList);
- }
-
- @Override
- public Page page(Pageable pageable) {
- return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
- }
-
- @Override
- public Stream stream() {
-
- return createSortedAndProjectedQuery() //
- .getResultStream() //
- .map(getConversionFunction());
- }
-
- @Override
- public long count() {
- return countOperation.apply(example);
- }
-
- @Override
- public boolean exists() {
- return existsOperation.apply(example);
- }
-
- private Page readPage(Pageable pageable) {
-
- TypedQuery pagedQuery = createSortedAndProjectedQuery();
-
- if (pageable.isPaged()) {
- pagedQuery.setFirstResult(PageableUtils.getOffsetAsInteger(pageable));
- pagedQuery.setMaxResults(pageable.getPageSize());
- }
-
- List paginatedResults = convert(pagedQuery.getResultList());
-
- return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(example));
- }
-
- private TypedQuery createSortedAndProjectedQuery() {
-
- TypedQuery query = finder.apply(sort);
-
- if (!properties.isEmpty()) {
- query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
- }
-
- return query;
- }
-
- private List convert(List resultList) {
-
- Function conversionFunction = getConversionFunction();
- List mapped = new ArrayList<>(resultList.size());
-
- for (S s : resultList) {
- mapped.add(conversionFunction.apply(s));
- }
- return mapped;
- }
-
- private Function getConversionFunction() {
- return getConversionFunction(example.getProbeType(), resultType);
- }
-
-}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java
index b70b3b4d42..1ac5affdea 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java
@@ -16,6 +16,7 @@
package org.springframework.data.jpa.repository.support;
import jakarta.persistence.EntityManager;
+import jakarta.persistence.Query;
import java.util.ArrayList;
import java.util.Collection;
@@ -29,7 +30,10 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Window;
+import org.springframework.data.jpa.repository.query.ScrollDelegate;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.util.Assert;
@@ -53,27 +57,31 @@ class FetchableFluentQueryByPredicate extends FluentQuerySupport imp
private final Predicate predicate;
private final Function> finder;
+
+ private final PredicateScrollDelegate scroll;
private final BiFunction> pagedFinder;
private final Function countOperation;
private final Function existsOperation;
private final EntityManager entityManager;
public FetchableFluentQueryByPredicate(Predicate predicate, Class entityType,
- Function> finder, BiFunction> pagedFinder,
- Function countOperation, Function existsOperation,
- EntityManager entityManager) {
- this(predicate, entityType, (Class) entityType, Sort.unsorted(), Collections.emptySet(), finder, pagedFinder,
- countOperation, existsOperation, entityManager);
+ Function> finder, PredicateScrollDelegate scroll,
+ BiFunction> pagedFinder, Function countOperation,
+ Function existsOperation, EntityManager entityManager) {
+ this(predicate, entityType, (Class) entityType, Sort.unsorted(), 0, Collections.emptySet(), finder, scroll,
+ pagedFinder, countOperation, existsOperation, entityManager);
}
private FetchableFluentQueryByPredicate(Predicate predicate, Class entityType, Class resultType, Sort sort,
- Collection properties, Function> finder,
- BiFunction> pagedFinder, Function countOperation,
- Function existsOperation, EntityManager entityManager) {
+ int limit, Collection properties, Function> finder,
+ PredicateScrollDelegate scroll, BiFunction> pagedFinder,
+ Function countOperation, Function existsOperation,
+ EntityManager entityManager) {
- super(resultType, sort, properties, entityType);
+ super(resultType, sort, limit, properties, entityType);
this.predicate = predicate;
this.finder = finder;
+ this.scroll = scroll;
this.pagedFinder = pagedFinder;
this.countOperation = countOperation;
this.existsOperation = existsOperation;
@@ -85,8 +93,17 @@ public FetchableFluentQuery sortBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
- return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, this.sort.and(sort), properties,
- finder, pagedFinder, countOperation, existsOperation, entityManager);
+ return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, this.sort.and(sort), limit,
+ properties, finder, scroll, pagedFinder, countOperation, existsOperation, entityManager);
+ }
+
+ @Override
+ public FetchableFluentQuery limit(int limit) {
+
+ Assert.isTrue(limit >= 0, "Limit must not be negative");
+
+ return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit, properties, finder,
+ scroll, pagedFinder, countOperation, existsOperation, entityManager);
}
@Override
@@ -98,15 +115,15 @@ public FetchableFluentQuery as(Class resultType) {
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
}
- return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, properties, finder,
- pagedFinder, countOperation, existsOperation, entityManager);
+ return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit, properties, finder,
+ scroll, pagedFinder, countOperation, existsOperation, entityManager);
}
@Override
public FetchableFluentQuery project(Collection properties) {
- return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, mergeProperties(properties),
- finder, pagedFinder, countOperation, existsOperation, entityManager);
+ return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit,
+ mergeProperties(properties), finder, scroll, pagedFinder, countOperation, existsOperation, entityManager);
}
@Override
@@ -138,6 +155,14 @@ public List all() {
return convert(createSortedAndProjectedQuery().fetch());
}
+ @Override
+ public Window scroll(ScrollPosition scrollPosition) {
+
+ Assert.notNull(scrollPosition, "ScrollPosition must not be null");
+
+ return scroll.scroll(sort, limit, scrollPosition).map(getConversionFunction());
+ }
+
@Override
public Page page(Pageable pageable) {
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
@@ -169,6 +194,10 @@ public boolean exists() {
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
}
+ if (limit != 0) {
+ query.limit(limit);
+ }
+
return query;
}
@@ -201,4 +230,24 @@ private Function getConversionFunction() {
return getConversionFunction(entityType, resultType);
}
+
+ static class PredicateScrollDelegate extends ScrollDelegate {
+
+ private final ScrollQueryFactory scrollFunction;
+
+ PredicateScrollDelegate(ScrollQueryFactory scrollQueryFactory, JpaEntityInformation entity) {
+ super(entity);
+ this.scrollFunction = scrollQueryFactory;
+ }
+
+ public Window scroll(Sort sort, int limit, ScrollPosition scrollPosition) {
+
+ Query query = scrollFunction.createQuery(sort, scrollPosition);
+ if (limit > 0) {
+ query = query.setMaxResults(limit);
+ }
+ return scroll(query, sort, scrollPosition);
+ }
+ }
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java
index 74d2d67afd..d8193f52a4 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java
@@ -16,6 +16,7 @@
package org.springframework.data.jpa.repository.support;
import jakarta.persistence.EntityManager;
+import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import java.util.ArrayList;
@@ -29,8 +30,11 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Window;
import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.repository.query.ScrollDelegate;
import org.springframework.data.jpa.support.PageableUtils;
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.data.support.PageableExecutionUtils;
@@ -50,26 +54,28 @@ class FetchableFluentQueryBySpecification extends FluentQuerySupport
private final Specification spec;
private final Function> finder;
+ private final SpecificationScrollDelegate scroll;
private final Function, Long> countOperation;
private final Function, Boolean> existsOperation;
private final EntityManager entityManager;
- public FetchableFluentQueryBySpecification(Specification spec, Class entityType, Sort sort,
- Collection properties, Function> finder,
+ public FetchableFluentQueryBySpecification(Specification spec, Class entityType,
+ Function> finder, SpecificationScrollDelegate scrollDelegate,
Function, Long> countOperation, Function, Boolean> existsOperation,
EntityManager entityManager) {
- this(spec, entityType, (Class) entityType, Sort.unsorted(), Collections.emptySet(), finder, countOperation,
- existsOperation, entityManager);
+ this(spec, entityType, (Class) entityType, Sort.unsorted(), 0, Collections.emptySet(), finder, scrollDelegate,
+ countOperation, existsOperation, entityManager);
}
private FetchableFluentQueryBySpecification(Specification spec, Class entityType, Class resultType,
- Sort sort, Collection properties, Function> finder,
- Function, Long> countOperation, Function, Boolean> existsOperation,
- EntityManager entityManager) {
+ Sort sort, int limit, Collection properties, Function> finder,
+ SpecificationScrollDelegate scrollDelegate, Function, Long> countOperation,
+ Function, Boolean> existsOperation, EntityManager entityManager) {
- super(resultType, sort, properties, entityType);
+ super(resultType, sort, limit, properties, entityType);
this.spec = spec;
this.finder = finder;
+ this.scroll = scrollDelegate;
this.countOperation = countOperation;
this.existsOperation = existsOperation;
this.entityManager = entityManager;
@@ -80,8 +86,17 @@ public FetchableFluentQuery sortBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
- return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, this.sort.and(sort), properties,
- finder, countOperation, existsOperation, entityManager);
+ return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, this.sort.and(sort), limit,
+ properties, finder, scroll, countOperation, existsOperation, entityManager);
+ }
+
+ @Override
+ public FetchableFluentQuery limit(int limit) {
+
+ Assert.isTrue(limit >= 0, "Limit must not be negative");
+
+ return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, this.sort.and(sort), limit,
+ properties, finder, scroll, countOperation, existsOperation, entityManager);
}
@Override
@@ -92,15 +107,15 @@ public FetchableFluentQuery as(Class resultType) {
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
}
- return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, properties, finder,
- countOperation, existsOperation, entityManager);
+ return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder,
+ scroll, countOperation, existsOperation, entityManager);
}
@Override
public FetchableFluentQuery project(Collection properties) {
- return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, properties, finder,
- countOperation, existsOperation, entityManager);
+ return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder,
+ scroll, countOperation, existsOperation, entityManager);
}
@Override
@@ -132,6 +147,14 @@ public List all() {
return convert(createSortedAndProjectedQuery().getResultList());
}
+ @Override
+ public Window scroll(ScrollPosition scrollPosition) {
+
+ Assert.notNull(scrollPosition, "ScrollPosition must not be null");
+
+ return scroll.scroll(sort, limit, scrollPosition).map(getConversionFunction());
+ }
+
@Override
public Page page(Pageable pageable) {
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
@@ -163,6 +186,10 @@ private TypedQuery createSortedAndProjectedQuery() {
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
}
+ if (limit != 0) {
+ query.setMaxResults(limit);
+ }
+
return query;
}
@@ -194,4 +221,25 @@ private List convert(List resultList) {
private Function getConversionFunction() {
return getConversionFunction(entityType, resultType);
}
+
+ static class SpecificationScrollDelegate extends ScrollDelegate {
+
+ private final ScrollQueryFactory scrollFunction;
+
+ SpecificationScrollDelegate(ScrollQueryFactory scrollQueryFactory, JpaEntityInformation entity) {
+ super(entity);
+ this.scrollFunction = scrollQueryFactory;
+ }
+
+ public Window scroll(Sort sort, int limit, ScrollPosition scrollPosition) {
+
+ Query query = scrollFunction.createQuery(sort, scrollPosition);
+
+ if (limit > 0) {
+ query = query.setMaxResults(limit);
+ }
+
+ return scroll(query, sort, scrollPosition);
+ }
+ }
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java
index 4ac26874a6..5aa8352ed8 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java
@@ -15,6 +15,8 @@
*/
package org.springframework.data.jpa.repository.support;
+import jakarta.persistence.Query;
+
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -22,6 +24,7 @@
import java.util.function.Function;
import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.lang.Nullable;
@@ -32,21 +35,25 @@
* @param The resulting type of the query.
* @author Greg Turnquist
* @author Jens Schauder
+ * @author Mark Paluch
* @since 2.6
*/
abstract class FluentQuerySupport {
protected final Class resultType;
protected final Sort sort;
+ protected final int limit;
protected final Set properties;
protected final Class entityType;
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
- FluentQuerySupport(Class resultType, Sort sort, @Nullable Collection properties, Class entityType) {
+ FluentQuerySupport(Class resultType, Sort sort, int limit, @Nullable Collection properties,
+ Class entityType) {
this.resultType = resultType;
this.sort = sort;
+ this.limit = limit;
if (properties != null) {
this.properties = new HashSet<>(properties);
@@ -78,4 +85,9 @@ final Function getConversionFunction(Class inputType, Class tar
return o -> DefaultConversionService.getSharedInstance().convert(o, targetType);
}
+
+ interface ScrollQueryFactory {
+ Query createQuery(Sort sort, ScrollPosition scrollPosition);
+ }
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformation.java
index f8b438ff82..f192386db0 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformation.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformation.java
@@ -17,6 +17,9 @@
import jakarta.persistence.metamodel.SingularAttribute;
+import java.util.Collection;
+import java.util.Map;
+
import org.springframework.data.jpa.repository.query.JpaEntityMetadata;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.lang.Nullable;
@@ -70,7 +73,7 @@ public interface JpaEntityInformation extends EntityInformation, J
*
* @return
*/
- Iterable getIdAttributeNames();
+ Collection getIdAttributeNames();
/**
* Extracts the value for the given id attribute from a composite id
@@ -81,4 +84,14 @@ public interface JpaEntityInformation extends EntityInformation, J
*/
@Nullable
Object getCompositeIdAttributeValue(Object id, String idAttribute);
+
+ /**
+ * Extract a keyset for {@code propertyPaths} and the primary key (including composite key components if applicable).
+ *
+ * @param propertyPaths
+ * @param entity
+ * @return
+ * @since 3.1
+ */
+ Map getKeyset(Iterable propertyPaths, T entity);
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java
index fa64f1cc51..c740887b27 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java
@@ -26,9 +26,12 @@
import jakarta.persistence.metamodel.Type;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -61,7 +64,7 @@ public class JpaMetamodelEntityInformation extends JpaEntityInformationSu
/**
* Creates a new {@link JpaMetamodelEntityInformation} for the given domain class and {@link Metamodel}.
- *
+ *
* @param domainClass must not be {@literal null}.
* @param metamodel must not be {@literal null}.
* @param persistenceUnitUtil must not be {@literal null}.
@@ -190,7 +193,7 @@ public boolean hasCompositeId() {
}
@Override
- public Iterable getIdAttributeNames() {
+ public Collection getIdAttributeNames() {
List attributeNames = new ArrayList<>(idMetadata.attributes.size());
@@ -222,6 +225,30 @@ public boolean isNew(T entity) {
return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
}
+ @Override
+ public Map getKeyset(Iterable propertyPaths, T entity) {
+
+ // TODO: Proxy handling requires more elaborate refactoring, see
+ // https://github.com/spring-projects/spring-data-jpa/issues/2784
+ BeanWrapper entityWrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
+
+ Map keyset = new LinkedHashMap<>();
+
+ if (hasCompositeId()) {
+ for (String idAttributeName : getIdAttributeNames()) {
+ keyset.put(idAttributeName, entityWrapper.getPropertyValue(idAttributeName));
+ }
+ } else {
+ keyset.put(getIdAttribute().getName(), getId(entity));
+ }
+
+ for (String propertyPath : propertyPaths) {
+ keyset.put(propertyPath, entityWrapper.getPropertyValue(propertyPath));
+ }
+
+ return keyset;
+ }
+
/**
* Simple value object to encapsulate id specific metadata.
*
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java
index 1f4c5883e7..2e38c4c2b3 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java
@@ -15,10 +15,10 @@
*/
package org.springframework.data.jpa.repository.support;
-import java.util.List;
-
import jakarta.persistence.EntityManager;
+import java.util.List;
+
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
@@ -245,4 +245,29 @@ private Expression> buildOrderPropertyPathFrom(Order order) {
return sortPropertyExpression;
}
+
+ /**
+ * Creates an {@link Expression} for the given {@code property} property.
+ *
+ * @param property must not be {@literal null}.
+ * @return
+ */
+ Expression> createExpression(String property) {
+
+ Assert.notNull(property, "Property must not be null");
+
+ PropertyPath path = PropertyPath.from(property, builder.getType());
+ Expression> sortPropertyExpression = builder;
+
+ while (path != null) {
+
+ sortPropertyExpression = !path.hasNext() && String.class.equals(path.getType()) //
+ ? Expressions.stringPath((Path>) sortPropertyExpression, path.getSegment()) //
+ : Expressions.path(path.getType(), (Path>) sortPropertyExpression, path.getSegment());
+
+ path = path.next();
+ }
+
+ return sortPropertyExpression;
+ }
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java
index 2396e39ae3..c899d051ab 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java
@@ -15,19 +15,27 @@
*/
package org.springframework.data.jpa.repository.support;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.LockModeType;
+
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.LockModeType;
-
import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.repository.EntityGraph;
+import org.springframework.data.jpa.repository.query.KeysetScrollDelegate;
+import org.springframework.data.jpa.repository.query.KeysetScrollDelegate.QueryStrategy;
+import org.springframework.data.jpa.repository.query.KeysetScrollSpecification;
+import org.springframework.data.jpa.repository.support.FetchableFluentQueryByPredicate.PredicateScrollDelegate;
+import org.springframework.data.jpa.repository.support.FluentQuerySupport.ScrollQueryFactory;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QSort;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
@@ -37,9 +45,14 @@
import org.springframework.util.Assert;
import com.querydsl.core.NonUniqueResultException;
+import com.querydsl.core.types.ConstantImpl;
import com.querydsl.core.types.EntityPath;
+import com.querydsl.core.types.Expression;
+import com.querydsl.core.types.NullExpression;
+import com.querydsl.core.types.Ops;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
+import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.JPQLQuery;
@@ -63,6 +76,7 @@ public class QuerydslJpaPredicateExecutor implements QuerydslPredicateExecuto
private final JpaEntityInformation entityInformation;
private final EntityPath path;
private final Querydsl querydsl;
+ private final QuerydslQueryStrategy scrollQueryAdapter;
private final EntityManager entityManager;
private final CrudMethodMetadata metadata;
@@ -83,6 +97,7 @@ public QuerydslJpaPredicateExecutor(JpaEntityInformation entityInformation
this.path = resolver.createPath(entityInformation.getJavaType());
this.querydsl = new Querydsl(entityManager, new PathBuilder(path.getType(), path.getMetadata()));
this.entityManager = entityManager;
+ this.scrollQueryAdapter = new QuerydslQueryStrategy();
}
@Override
@@ -160,6 +175,33 @@ public R findBy(Predicate predicate, Function scroll = (sort, scrollPosition) -> {
+
+ Predicate predicateToUse = predicate;
+
+ if (scrollPosition instanceof KeysetScrollPosition keyset) {
+
+ KeysetScrollDelegate delegate = KeysetScrollDelegate.of(keyset.getDirection());
+ sort = KeysetScrollSpecification.createSort(keyset, sort, entityInformation);
+ BooleanExpression keysetPredicate = delegate.createPredicate(keyset, sort, scrollQueryAdapter);
+
+ if (keysetPredicate != null) {
+ predicateToUse = predicate instanceof BooleanExpression be ? be.and(keysetPredicate)
+ : keysetPredicate.and(predicate);
+ }
+ }
+
+ AbstractJPAQuery, ?> select = (AbstractJPAQuery, ?>) createQuery(predicateToUse).select(path);
+
+ select = (AbstractJPAQuery, ?>) querydsl.applySorting(sort, select);
+
+ if (scrollPosition instanceof OffsetScrollPosition offset) {
+ select.offset(offset.getOffset());
+ }
+
+ return select.createQuery();
+ };
+
BiFunction> pagedFinder = (sort, pageable) -> {
AbstractJPAQuery, ?> select = finder.apply(sort);
@@ -175,6 +217,7 @@ public R findBy(Predicate predicate, Function(scroll, entityInformation), //
pagedFinder, //
this::count, //
this::exists, //
@@ -285,4 +328,34 @@ private List executeSorted(JPQLQuery query, OrderSpecifier>... orders) {
private List executeSorted(JPQLQuery query, Sort sort) {
return querydsl.applySorting(sort, query).fetch();
}
+
+ class QuerydslQueryStrategy implements QueryStrategy, BooleanExpression> {
+
+ @Override
+ public Expression> createExpression(String property) {
+ return querydsl.createExpression(property);
+ }
+
+ @Override
+ public BooleanExpression compare(Order order, Expression> propertyExpression, Object value) {
+ return Expressions.booleanOperation(order.isAscending() ? Ops.GT : Ops.LT, propertyExpression,
+ ConstantImpl.create(value));
+ }
+
+ @Override
+ public BooleanExpression compare(Expression> propertyExpression, @Nullable Object value) {
+ return Expressions.booleanOperation(Ops.EQ, propertyExpression,
+ value == null ? NullExpression.DEFAULT : ConstantImpl.create(value));
+ }
+
+ @Override
+ public BooleanExpression and(List intermediate) {
+ return Expressions.allOf(intermediate.toArray(new BooleanExpression[0]));
+ }
+
+ @Override
+ public BooleanExpression or(List intermediate) {
+ return Expressions.anyOf(intermediate.toArray(new BooleanExpression[0]));
+ }
+ }
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
index d83d5f50ff..788ea6fae3 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
@@ -43,6 +43,8 @@
import java.util.stream.StreamSupport;
import org.springframework.data.domain.Example;
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
@@ -52,7 +54,10 @@
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
+import org.springframework.data.jpa.repository.query.KeysetScrollSpecification;
import org.springframework.data.jpa.repository.query.QueryUtils;
+import org.springframework.data.jpa.repository.support.FetchableFluentQueryBySpecification.SpecificationScrollDelegate;
+import org.springframework.data.jpa.repository.support.FluentQuerySupport.ScrollQueryFactory;
import org.springframework.data.jpa.repository.support.QueryHints.NoHints;
import org.springframework.data.jpa.support.PageableUtils;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
@@ -507,10 +512,40 @@ public