queryCache = new ConcurrentLruCache<>(16,
AbstractStringBasedJpaQuery.this::applySorting);
+ private volatile String cachedQueryString;
+
@Override
- public String getSorted(DeclaredQuery query, Sort sort) {
+ public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
if (sort.isUnsorted()) {
- return query.getQueryString();
+
+ String cachedQueryString = this.cachedQueryString;
+ if (cachedQueryString == null) {
+ this.cachedQueryString = cachedQueryString = queryCache.get(new CachableQuery(query, sort, returnedType));
+ }
+
+ return cachedQueryString;
}
- return queryCache.get(new CachableQuery(query, sort));
+ return queryCache.get(new CachableQuery(query, sort, returnedType));
}
}
@@ -269,12 +297,14 @@ static class CachableQuery {
private final DeclaredQuery declaredQuery;
private final String queryString;
private final Sort sort;
+ private final ReturnedType returnedType;
- CachableQuery(DeclaredQuery query, Sort sort) {
+ CachableQuery(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
this.declaredQuery = query;
this.queryString = query.getQueryString();
this.sort = sort;
+ this.returnedType = returnedType;
}
DeclaredQuery getDeclaredQuery() {
@@ -285,9 +315,8 @@ Sort getSort() {
return sort;
}
- @Nullable
- String getAlias() {
- return declaredQuery.getAlias();
+ public ReturnedType getReturnedType() {
+ return returnedType;
}
@Override
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java
index d8c5bb4a50..8ec778fb70 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java
@@ -52,6 +52,11 @@ public String applySorting(Sort sort, @Nullable String alias) {
return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
}
+ @Override
+ public String rewrite(QueryRewriteInformation rewriteInformation) {
+ return QueryUtils.applySorting(this.query.getQueryString(), rewriteInformation.getSort(), alias);
+ }
+
@Override
public String createCountQueryFor(@Nullable String countProjection) {
return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, this.query.isNativeQuery());
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryRewriteInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryRewriteInformation.java
new file mode 100644
index 0000000000..ee17ca3f04
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryRewriteInformation.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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 org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.ReturnedType;
+
+/**
+ * Default {@link org.springframework.data.jpa.repository.query.QueryEnhancer.QueryRewriteInformation} implementation.
+ *
+ * @author Mark Paluch
+ */
+record DefaultQueryRewriteInformation(Sort sort,
+ ReturnedType returnedType) implements QueryEnhancer.QueryRewriteInformation {
+ @Override
+ public Sort getSort() {
+ return sort();
+ }
+
+ @Override
+ public ReturnedType getReturnedType() {
+ return returnedType();
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java
new file mode 100644
index 0000000000..c87a5d63de
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 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 static org.springframework.data.jpa.repository.query.QueryTokens.*;
+
+import org.springframework.data.repository.query.ReturnedType;
+
+/**
+ * HQL Query Transformer that rewrites the query using constructor expressions.
+ *
+ * Query rewriting from a plain property/object selection towards constructor expression only works if either:
+ *
+ * - The query selects its primary alias ({@code SELECT p FROM Person p})
+ * - The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p})
+ *
+ *
+ * @author Mark Paluch
+ * @since 3.5
+ */
+class DtoProjectionTransformerDelegate {
+
+ private final ReturnedType returnedType;
+
+ public DtoProjectionTransformerDelegate(ReturnedType returnedType) {
+ this.returnedType = returnedType;
+ }
+
+ public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) {
+
+ if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface()
+ || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
+ return selectionList;
+ }
+
+ QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder();
+ builder.append(QueryTokens.TOKEN_NEW);
+ builder.append(QueryTokens.token(returnedType.getReturnedType().getName()));
+ builder.append(QueryTokens.TOKEN_OPEN_PAREN);
+
+ // assume the selection points to the document
+ if (selectionList.size() == 1) {
+
+ builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> {
+
+ QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder();
+ prop.append(QueryTokens.token(selectionList.getFirst().value()));
+ prop.append(QueryTokens.TOKEN_DOT);
+ prop.append(QueryTokens.token(property));
+
+ return prop.build();
+ }, QueryTokens.TOKEN_COMMA));
+
+ } else {
+ builder.appendInline(selectionList);
+ }
+
+ builder.append(QueryTokens.TOKEN_CLOSE_PAREN);
+
+ return builder.build();
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java
index ed14e9afdf..edd906f07f 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java
@@ -21,6 +21,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
+import org.springframework.data.repository.query.ReturnedType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -40,13 +41,15 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer {
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
private final Sort sort;
private final @Nullable String primaryFromAlias;
+ private final @Nullable DtoProjectionTransformerDelegate dtoDelegate;
- EqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
+ EqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
+ this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType);
}
@Override
@@ -80,6 +83,26 @@ public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementCont
return builder;
}
+ @Override
+ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) {
+
+ if (dtoDelegate == null) {
+ return super.visitSelect_clause(ctx);
+ }
+
+ QueryRendererBuilder builder = QueryRenderer.builder();
+
+ builder.append(QueryTokens.expression(ctx.SELECT()));
+
+ if (ctx.DISTINCT() != null) {
+ builder.append(QueryTokens.expression(ctx.DISTINCT()));
+ }
+
+ QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA);
+
+ return builder.append(dtoDelegate.transformSelectionList(tokenStream));
+ }
+
private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx, Sort sort) {
if (ctx.orderby_clause() != null) {
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java
index b6b8853f93..9953b3e6c1 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java
@@ -21,6 +21,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
+import org.springframework.data.repository.query.ReturnedType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -38,6 +39,7 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
private final Sort sort;
private final @Nullable String primaryFromAlias;
+ private final @Nullable DtoProjectionTransformerDelegate dtoDelegate;
HqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
@@ -45,6 +47,16 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
+ this.dtoDelegate = null;
+ }
+
+ HqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) {
+
+ Assert.notNull(sort, "Sort must not be null");
+
+ this.sort = sort;
+ this.primaryFromAlias = primaryFromAlias;
+ this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType);
}
@Override
@@ -81,6 +93,18 @@ public QueryRendererBuilder visitOrderedQuery(HqlParser.OrderedQueryContext ctx)
return visitOrderedQuery(ctx, this.sort);
}
+ @Override
+ public QueryTokenStream visitSelectionList(HqlParser.SelectionListContext ctx) {
+
+ QueryTokenStream tokenStream = super.visitSelectionList(ctx);
+
+ if (dtoDelegate != null && !isSubquery(ctx)) {
+ return dtoDelegate.transformSelectionList(tokenStream);
+ }
+
+ return tokenStream;
+ }
+
@Override
public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) {
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java
index 37ec06e12f..f9ebe1efa7 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java
@@ -297,12 +297,20 @@ public DeclaredQuery getQuery() {
@Override
public String applySorting(Sort sort) {
- return applySorting(sort, detectAlias());
+ return doApplySorting(sort, detectAlias());
+ }
+
+ @Override
+ public String rewrite(QueryRewriteInformation rewriteInformation) {
+ return doApplySorting(rewriteInformation.getSort(), primaryAlias);
}
@Override
public String applySorting(Sort sort, @Nullable String alias) {
+ return doApplySorting(sort, alias);
+ }
+ private String doApplySorting(Sort sort, @Nullable String alias) {
String queryString = query.getQueryString();
Assert.hasText(queryString, "Query must not be null or empty");
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java
index 25e7c25ca9..9d767d004f 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java
@@ -21,8 +21,6 @@
import jakarta.persistence.criteria.Root;
import java.util.Collection;
-import java.util.LinkedHashSet;
-import java.util.Set;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Sort;
@@ -77,9 +75,7 @@ Collection getRequiredSelection(Sort sort, ReturnedType returnedType) {
Sort sortToUse = KeysetScrollSpecification.createSort(scrollPosition, sort, entityInformation);
- Set selection = new LinkedHashSet<>(returnedType.getInputProperties());
- sortToUse.forEach(it -> selection.add(it.getProperty()));
-
- return selection;
+ return KeysetScrollDelegate.getProjectionInputProperties(entityInformation, returnedType.getInputProperties(),
+ sortToUse);
}
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java
index c677b2efcc..f5ed753c97 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java
@@ -30,6 +30,7 @@
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.ReturnedType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -48,12 +49,12 @@ class JpaQueryEnhancer implements QueryEnhancer {
private final ParserRuleContext context;
private final ParsedQueryIntrospector introspector;
private final String projection;
- private final BiFunction> sortFunction;
+ private final SortedQueryRewriteFunction sortFunction;
private final BiFunction> countQueryFunction;
JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector introspector,
- @Nullable BiFunction> sortFunction,
- @Nullable BiFunction> countQueryFunction) {
+ SortedQueryRewriteFunction sortFunction,
+ BiFunction> countQueryFunction) {
this.context = context;
this.introspector = introspector;
@@ -136,6 +137,10 @@ public static JpaQueryEnhancer forEql(DeclaredQuery query) {
return EqlQueryParser.parseQuery(query.getQueryString());
}
+ ParserRuleContext getContext() {
+ return context;
+ }
+
/**
* Checks if the select clause has a new constructor instantiation in the JPA query.
*
@@ -191,7 +196,13 @@ public DeclaredQuery getQuery() {
*/
@Override
public String applySorting(Sort sort) {
- return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, detectAlias()).visit(context));
+ return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, detectAlias(), null).visit(context));
+ }
+
+ @Override
+ public String rewrite(QueryRewriteInformation rewriteInformation) {
+ return QueryRenderer.TokenRenderer.render(sortFunction
+ .apply(rewriteInformation.getSort(), detectAlias(), rewriteInformation.getReturnedType()).visit(context));
}
/**
@@ -308,4 +319,17 @@ public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarExce
return new JpqlQueryParser(query);
}
}
+
+ /**
+ * Functional interface to rewrite a query considering {@link Sort} and {@link ReturnedType}. The function returns a
+ * visitor object that can visit the parsed query tree.
+ *
+ * @since 3.5
+ */
+ @FunctionalInterface
+ interface SortedQueryRewriteFunction {
+
+ ParseTreeVisitor extends Object> apply(Sort sort, String primaryAlias, @Nullable ReturnedType returnedType);
+
+ }
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java
index a545171bbf..2539322498 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java
@@ -21,6 +21,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
+import org.springframework.data.repository.query.ReturnedType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -38,6 +39,7 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
private final Sort sort;
private final @Nullable String primaryFromAlias;
+ private final @Nullable DtoProjectionTransformerDelegate dtoDelegate;
JpqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
@@ -45,6 +47,16 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
+ this.dtoDelegate = null;
+ }
+
+ JpqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) {
+
+ Assert.notNull(sort, "Sort must not be null");
+
+ this.sort = sort;
+ this.primaryFromAlias = primaryFromAlias;
+ this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType);
}
@Override
@@ -72,6 +84,26 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext
return builder;
}
+ @Override
+ public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) {
+
+ if (dtoDelegate == null) {
+ return super.visitSelect_clause(ctx);
+ }
+
+ QueryRendererBuilder builder = QueryRenderer.builder();
+
+ builder.append(QueryTokens.expression(ctx.SELECT()));
+
+ if (ctx.DISTINCT() != null) {
+ builder.append(QueryTokens.expression(ctx.DISTINCT()));
+ }
+
+ QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA);
+
+ return builder.append(dtoDelegate.transformSelectionList(tokenStream));
+ }
+
private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) {
if (ctx.orderby_clause() != null) {
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java
index 2942fa0bce..66b9245b60 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java
@@ -16,7 +16,9 @@
package org.springframework.data.jpa.repository.query;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -24,6 +26,7 @@
import org.springframework.data.domain.ScrollPosition.Direction;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.lang.Nullable;
/**
@@ -47,6 +50,25 @@ public static KeysetScrollDelegate of(Direction direction) {
return direction == Direction.FORWARD ? FORWARD : REVERSE;
}
+ /**
+ * Return a collection of property names required to construct a keyset selection query that include all keyset and
+ * identifier properties required to resume keyset scrolling.
+ *
+ * @param entity the underlying entity.
+ * @param projectionProperties projection property names.
+ * @param sort sort properties.
+ * @return a collection of property names required to construct a keyset selection query
+ */
+ public static Collection getProjectionInputProperties(JpaEntityInformation, ?> entity,
+ Collection projectionProperties, Sort sort) {
+
+ Collection properties = new LinkedHashSet<>(projectionProperties);
+ sort.forEach(it -> properties.add(it.getProperty()));
+ properties.addAll(entity.getIdAttributeNames());
+
+ return properties;
+ }
+
@Nullable
public P createPredicate(KeysetScrollPosition keyset, Sort sort, QueryStrategy strategy) {
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java
index 55c168a4f5..3697c22980 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java
@@ -18,6 +18,7 @@
import java.util.Set;
import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.ReturnedType;
import org.springframework.lang.Nullable;
/**
@@ -81,10 +82,20 @@ public interface QueryEnhancer {
* @param sort the sort specification to apply.
* @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
* @return the modified query string.
+ * @deprecated since 3.5, use {@link #rewrite(QueryRewriteInformation)} instead.
*/
- @Deprecated
+ @Deprecated(since = "3.5", forRemoval = true)
String applySorting(Sort sort, @Nullable String alias);
+ /**
+ * Rewrite the query to include sorting and apply {@link ReturnedType} customizations.
+ *
+ * @param rewriteInformation the rewrite information to apply.
+ * @return the modified query string.
+ * @since 3.5
+ */
+ String rewrite(QueryRewriteInformation rewriteInformation);
+
/**
* Creates a count projected query from the given original query.
*
@@ -101,4 +112,23 @@ default String createCountQueryFor() {
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
*/
String createCountQueryFor(@Nullable String countProjection);
+
+ /**
+ * Interface to describe the information needed to rewrite a query.
+ *
+ * @since 3.5
+ */
+ interface QueryRewriteInformation {
+
+ /**
+ * @return the sort specification to apply.
+ */
+ Sort getSort();
+
+ /**
+ * @return type expected to be returned by the query.
+ */
+ ReturnedType getReturnedType();
+ }
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java
index 80df0b3300..cc339f4adf 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java
@@ -38,6 +38,7 @@ class QueryTokens {
static final QueryToken TOKEN_EQUALS = token(" = ");
static final QueryToken TOKEN_OPEN_PAREN = token("(");
static final QueryToken TOKEN_CLOSE_PAREN = token(")");
+ static final QueryToken TOKEN_NEW = expression("new");
static final QueryToken TOKEN_ORDER_BY = expression("order by");
static final QueryToken TOKEN_LOWER_FUNC = token("lower(");
static final QueryToken TOKEN_SELECT_COUNT = token("select count(");
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTransformers.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTransformers.java
index 46bdc36003..b8f06d1368 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTransformers.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTransformers.java
@@ -61,7 +61,7 @@ static CountSelectionTokenStream create(QueryTokenStream selection) {
token = QueryTokens.token(token.value());
}
- if (!containsNew && token.value().contains("new")) {
+ if (!containsNew && token.equals(TOKEN_NEW)) {
containsNew = true;
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
index 3c06c7079b..ad812a5e84 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
@@ -769,7 +769,8 @@ static Expression toExpressionRecursively(From, ?> from, PropertyPath p
return toExpressionRecursively(from, property, false);
}
- static Expression toExpressionRecursively(From, ?> from, PropertyPath property, boolean isForSelection) {
+ public static Expression toExpressionRecursively(From, ?> from, PropertyPath property,
+ boolean isForSelection) {
return toExpressionRecursively(from, property, isForSelection, false);
}
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 9ed0a0ce3e..4302c63650 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,7 +16,6 @@
package org.springframework.data.jpa.repository.support;
import jakarta.persistence.EntityManager;
-import jakarta.persistence.Query;
import java.util.ArrayList;
import java.util.Collection;
@@ -27,19 +26,29 @@
import java.util.stream.Stream;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.data.domain.KeysetScrollPosition;
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.KeysetScrollDelegate;
import org.springframework.data.jpa.repository.query.ScrollDelegate;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
+import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.support.PageableExecutionUtils;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
+import com.querydsl.core.types.EntityPath;
+import com.querydsl.core.types.Expression;
+import com.querydsl.core.types.ExpressionBase;
import com.querydsl.core.types.Predicate;
+import com.querydsl.core.types.Visitor;
+import com.querydsl.core.types.dsl.PathBuilder;
+import com.querydsl.jpa.JPQLSerializer;
import com.querydsl.jpa.impl.AbstractJPAQuery;
/**
@@ -57,33 +66,40 @@
*/
class FetchableFluentQueryByPredicate extends FluentQuerySupport implements FetchableFluentQuery {
+ private final EntityPath> entityPath;
+ private final JpaEntityInformation entityInformation;
+ private final ScrollQueryFactory> scrollQueryFactory;
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;
- FetchableFluentQueryByPredicate(Predicate predicate, Class entityType,
- Function> finder, PredicateScrollDelegate scroll,
+ FetchableFluentQueryByPredicate(EntityPath> entityPath, Predicate predicate,
+ JpaEntityInformation entityInformation, Function> finder,
+ ScrollQueryFactory> scrollQueryFactory,
BiFunction> pagedFinder, Function countOperation,
Function existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) {
- this(predicate, entityType, (Class) entityType, Sort.unsorted(), 0, Collections.emptySet(), finder, scroll,
- pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
+ this(entityPath, predicate, entityInformation, (Class) entityInformation.getJavaType(), Sort.unsorted(), 0,
+ Collections.emptySet(), finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
+ projectionFactory);
}
- private FetchableFluentQueryByPredicate(Predicate predicate, Class entityType, Class resultType, Sort sort,
- int limit, Collection properties, Function> finder,
- PredicateScrollDelegate scroll, BiFunction> pagedFinder,
- Function countOperation, Function existsOperation,
- EntityManager entityManager, ProjectionFactory projectionFactory) {
+ private FetchableFluentQueryByPredicate(EntityPath> entityPath, Predicate predicate,
+ JpaEntityInformation entityInformation, Class resultType, Sort sort, int limit,
+ Collection properties, Function> finder,
+ ScrollQueryFactory> scrollQueryFactory,
+ BiFunction> pagedFinder, Function countOperation,
+ Function existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) {
- super(resultType, sort, limit, properties, entityType, projectionFactory);
+ super(resultType, sort, limit, properties, entityInformation.getJavaType(), projectionFactory);
+ this.entityInformation = entityInformation;
+ this.entityPath = entityPath;
this.predicate = predicate;
this.finder = finder;
- this.scroll = scroll;
+ this.scrollQueryFactory = scrollQueryFactory;
this.pagedFinder = pagedFinder;
this.countOperation = countOperation;
this.existsOperation = existsOperation;
@@ -95,8 +111,9 @@ public FetchableFluentQuery sortBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
- return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, this.sort.and(sort), limit,
- properties, finder, scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
+ return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType,
+ this.sort.and(sort), limit, properties, finder, scrollQueryFactory, pagedFinder, countOperation,
+ existsOperation, entityManager, projectionFactory);
}
@Override
@@ -104,8 +121,9 @@ 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, projectionFactory);
+ return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
+ properties, finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
+ projectionFactory);
}
@Override
@@ -113,20 +131,17 @@ 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 FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit, properties, finder,
- scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
+ return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
+ properties, finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
+ projectionFactory);
}
@Override
public FetchableFluentQuery project(Collection properties) {
- return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit,
- mergeProperties(properties), finder, scroll, pagedFinder, countOperation, existsOperation, entityManager,
- projectionFactory);
+ return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
+ mergeProperties(properties), finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation,
+ entityManager, projectionFactory);
}
@Override
@@ -163,7 +178,8 @@ public Window scroll(ScrollPosition scrollPosition) {
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
- return scroll.scroll(sort, limit, scrollPosition).map(getConversionFunction());
+ return new PredicateScrollDelegate<>(scrollQueryFactory, entityInformation)
+ .scroll(returnedType, sort, limit, scrollPosition).map(getConversionFunction());
}
@Override
@@ -192,6 +208,33 @@ public boolean exists() {
private AbstractJPAQuery, ?> createSortedAndProjectedQuery() {
AbstractJPAQuery, ?> query = finder.apply(sort);
+ applyQuerySettings(this.returnedType, this.limit, query, null);
+ return query;
+ }
+
+ private void applyQuerySettings(ReturnedType returnedType, int limit, AbstractJPAQuery, ?> query,
+ @Nullable ScrollPosition scrollPosition) {
+
+ List inputProperties = returnedType.getInputProperties();
+
+ if (returnedType.needsCustomConstruction()) {
+
+ Collection requiredSelection;
+ if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) {
+ requiredSelection = KeysetScrollDelegate.getProjectionInputProperties(entityInformation, inputProperties, sort);
+ } else {
+ requiredSelection = inputProperties;
+ }
+
+ PathBuilder> builder = new PathBuilder<>(entityPath.getType(), entityPath.getMetadata());
+ Expression>[] projection = requiredSelection.stream().map(builder::get).toArray(Expression[]::new);
+
+ if (returnedType.getReturnedType().isInterface()) {
+ query.select(new JakartaTuple(projection));
+ } else {
+ query.select(new DtoProjection(returnedType.getReturnedType(), projection));
+ }
+ }
if (!properties.isEmpty()) {
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
@@ -200,8 +243,6 @@ public boolean exists() {
if (limit != 0) {
query.limit(limit);
}
-
- return query;
}
private Page readPage(Pageable pageable) {
@@ -233,23 +274,60 @@ private Function