From 1341c3f14e134757c3a1cbf11e220a2efc3e1886 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 Aug 2024 14:25:08 +0200 Subject: [PATCH] Rewrite string-queries to use constructor expressions when return type is DTO. We now rewrite String-based JPA queries to use constructor expressions when either selecting the entity or selecting individual properties. We do not rewrite queries that already use constructor expressions. Closes: #3076 Original Pull Request: #3654 --- .../repository/query/AbstractJpaQuery.java | 3 +- .../query/AbstractStringBasedJpaQuery.java | 63 +++++++--- .../query/DefaultQueryEnhancer.java | 6 + .../DtoProjectionTransformerDelegate.java | 73 ++++++++++++ .../query/EqlSortedQueryTransformer.java | 25 +++- .../query/HqlSortedQueryTransformer.java | 24 ++++ .../query/JSqlParserQueryEnhancer.java | 6 + .../repository/query/JpaQueryEnhancer.java | 24 +++- .../query/JpqlSortedQueryTransformer.java | 32 +++++ .../jpa/repository/query/QueryEnhancer.java | 4 + .../jpa/repository/query/QueryTokens.java | 1 + .../repository/query/QueryTransformers.java | 2 +- .../repository/UserRepositoryFinderTests.java | 20 +++- .../AbstractStringBasedJpaQueryUnitTests.java | 8 +- .../EqlDtoQueryTransformerUnitTests.java | 112 ++++++++++++++++++ .../HqlDtoQueryTransformerUnitTests.java | 112 ++++++++++++++++++ .../JpqlDtoQueryTransformerUnitTests.java | 112 ++++++++++++++++++ .../query/NativeJpaQueryUnitTests.java | 15 +-- .../jpa/repository/sample/UserRepository.java | 10 ++ .../ROOT/pages/repositories/projections.adoc | 35 ++++++ 20 files changed, 650 insertions(+), 37 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index bbde6d941..909053364 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -283,7 +283,8 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { return null; } - return returnedType.isProjecting() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) // + return returnedType.isProjecting() && returnedType.getReturnedType().isInterface() + && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) // ? Tuple.class // : null; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index 0d257fe5a..16ab1d21a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -99,10 +99,13 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { }); this.queryRewriter = queryRewriter; + ReturnedType returnedType = method.getResultProcessor().getReturnedType(); JpaParameters parameters = method.getParameters(); - if (parameters.hasPageableParameter() || parameters.hasSortParameter()) { + if ((parameters.hasPageableParameter() || parameters.hasSortParameter()) && !parameters.hasDynamicProjection()) { this.querySortRewriter = new CachingQuerySortRewriter(); + } else if (returnedType.isProjecting() && !returnedType.getReturnedType().isInterface()) { + this.querySortRewriter = new ProjectingSortRewriter(); } else { this.querySortRewriter = NoOpQuerySortRewriter.INSTANCE; } @@ -115,9 +118,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { public Query doCreateQuery(JpaParametersParameterAccessor accessor) { Sort sort = accessor.getSort(); - String sortedQueryString = getSortedQueryString(sort); - ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + String sortedQueryString = getSortedQueryString(sort, processor.getReturnedType()); Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), processor.getReturnedType()); @@ -128,8 +130,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { return parameterBinder.get().bindAndPrepare(query, metadata, accessor); } - String getSortedQueryString(Sort sort) { - return querySortRewriter.getSorted(query, sort); + String getSortedQueryString(Sort sort, ReturnedType returnedType) { + return querySortRewriter.getSorted(query, sort, returnedType); } @Override @@ -211,24 +213,25 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { String applySorting(CachableQuery cachableQuery) { - return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).applySorting(cachableQuery.getSort(), - cachableQuery.getAlias()); + return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).rewrite(cachableQuery.getSort(), + cachableQuery.getReturnedType()); } /** * Query Sort Rewriter interface. */ interface QuerySortRewriter { - String getSorted(DeclaredQuery query, Sort sort); + String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType); } /** * No-op query rewriter. */ enum NoOpQuerySortRewriter implements QuerySortRewriter { + INSTANCE; - public String getSorted(DeclaredQuery query, Sort sort) { + public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { if (sort.isSorted()) { throw new UnsupportedOperationException("NoOpQueryCache does not support sorting"); @@ -238,6 +241,25 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { } } + static class ProjectingSortRewriter implements QuerySortRewriter { + + private volatile String cachedQueryString; + + public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { + + if (sort.isSorted()) { + throw new UnsupportedOperationException("NoOpQueryCache does not support sorting"); + } + + String cachedQueryString = this.cachedQueryString; + if (cachedQueryString == null) { + this.cachedQueryString = cachedQueryString = QueryEnhancerFactory.forQuery(query).rewrite(sort, returnedType); + } + + return cachedQueryString; + } + } + /** * Caching variant of {@link QuerySortRewriter}. */ @@ -246,14 +268,22 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { private final ConcurrentLruCache 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 +299,14 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { 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 +317,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { 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 d8c5bb4a5..07ef7d15b 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 @@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query; import java.util.Set; import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.lang.Nullable; /** @@ -52,6 +53,11 @@ public class DefaultQueryEnhancer implements QueryEnhancer { return QueryUtils.applySorting(this.query.getQueryString(), sort, alias); } + @Override + public String rewrite(Sort sort, ReturnedType returnedType) { + return QueryUtils.applySorting(this.query.getQueryString(), sort, 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/DtoProjectionTransformerDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java new file mode 100644 index 000000000..558575f15 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java @@ -0,0 +1,73 @@ +/* + * 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: + *

+ * + * @author Mark Paluch + */ +class DtoProjectionTransformerDelegate { + + private final ReturnedType returnedType; + + public DtoProjectionTransformerDelegate(ReturnedType returnedType) { + this.returnedType = returnedType; + } + + public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) { + + if (!returnedType.isProjecting() || 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 ed14e9afd..edd906f07 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 java.util.List; 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 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer { 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 b6b8853f9..9953b3e6c 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 java.util.List; 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 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer { 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 168614860..1fe86ea11 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 @@ -50,6 +50,7 @@ import java.util.Set; import java.util.StringJoiner; import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -300,6 +301,11 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { return applySorting(sort, detectAlias()); } + @Override + public String rewrite(Sort sort, ReturnedType returnedType) { + return applySorting(sort, primaryAlias); + } + @Override public String applySorting(Sort sort, @Nullable String alias) { 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 c677b2efc..ad0597983 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.TokenStream; 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 @@ class JpaQueryEnhancer implements QueryEnhancer { 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,12 @@ class JpaQueryEnhancer implements QueryEnhancer { */ @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(Sort sort, ReturnedType returnedType) { + return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, detectAlias(), returnedType).visit(context)); } /** @@ -308,4 +318,10 @@ class JpaQueryEnhancer implements QueryEnhancer { return new JpqlQueryParser(query); } } + + interface SortedQueryRewriteFunction { + + ParseTreeVisitor 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 a545171bb..253932249 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 java.util.List; 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 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { 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/QueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java index 55c168a4f..ba0dfcf65 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 @@ package org.springframework.data.jpa.repository.query; import java.util.Set; import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.lang.Nullable; /** @@ -85,6 +86,8 @@ public interface QueryEnhancer { @Deprecated String applySorting(Sort sort, @Nullable String alias); + String rewrite(Sort sort, ReturnedType returnedType); + /** * Creates a count projected query from the given original query. * @@ -101,4 +104,5 @@ public interface QueryEnhancer { * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}. */ String createCountQueryFor(@Nullable String countProjection); + } 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 80df0b330..cc339f4ad 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 46bdc3600..b8f06d136 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 @@ class QueryTransformers { 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/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java index fdf3330ca..d887cc51b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java @@ -378,7 +378,7 @@ class UserRepositoryFinderTests { } @Test // DATAJPA-1713, GH-2008 - public void selectProjectionWithSubselect() { + void selectProjectionWithSubselect() { List dtos = userRepository.findProjectionBySubselect(); @@ -405,4 +405,22 @@ class UserRepositoryFinderTests { result = userRepository.findUserByLastname(carter.getLastname()); assertThat(result).containsExactly(carter); } + + @Test // GH-3076 + void dtoProjectionShouldApplyConstructorExpressionRewriting() { + + List dtos = userRepository.findRecordProjection(); + + assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) // + .contains("Dave", "Carter", "Oliver August"); + } + + @Test // GH-3076 + void dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() { + + List dtos = userRepository.findMultiselectRecordProjection(); + + assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) // + .contains("Dave", "Carter", "Oliver August"); + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java index b2e6ba4fc..9611cd11e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java @@ -52,6 +52,7 @@ import org.springframework.util.ReflectionUtils; * Unit tests for {@link AbstractStringBasedJpaQuery}. * * @author Christoph Strobl + * @author Mark Paluch */ class AbstractStringBasedJpaQueryUnitTests { @@ -64,13 +65,14 @@ class AbstractStringBasedJpaQueryUnitTests { stringQuery.neverCalled("applySorting"); } - @Test // GH-3310 - void shouldNotAttemptToAppendSortIfSortIndicatesUnsorted() { + @Test // GH-3310, GH-3076 + void shouldRunQueryRewriterOnce() { InvocationCapturingStringQueryStub stringQuery = forMethod(TestRepo.class, "find", Sort.class); stringQuery.createQueryWithArguments(Sort.unsorted()); + stringQuery.createQueryWithArguments(Sort.unsorted()); - stringQuery.neverCalled("applySorting"); + stringQuery.called("applySorting").times(1); } @Test // GH-3310 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java new file mode 100644 index 000000000..4e0a7ead4 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java @@ -0,0 +1,112 @@ +/* + * 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.assertj.core.api.Assertions.*; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * Unit tests for {@link DtoProjectionTransformerDelegate}. + * + * @author Mark Paluch + */ +class EqlDtoQueryTransformerUnitTests { + + JpaQueryMethod method = getMethod("dtoProjection"); + EqlSortedQueryTransformer transformer = new EqlSortedQueryTransformer(Sort.unsorted(), null, + method.getResultProcessor().getReturnedType()); + + @Test // GH-3076 + void shouldTranslateSingleProjectionToDto() { + + JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery("SELECT p from Person p"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); + } + + @Test // GH-3076 + void shouldRewriteQueriesWithSubselect() { + + JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser + .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "select new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); + } + + @Test // GH-3076 + void shouldNotTranslateConstructorExpressionQuery() { + + JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser + .parseQuery("SELECT NEW String(p) from Person p"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); + } + + @Test + void shouldTranslatePropertySelectionToDto() { + + JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser + .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); + } + + private JpaQueryMethod getMethod(String name, Class... parameterTypes) { + + try { + Method method = MyRepo.class.getMethod(name, parameterTypes); + PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; + + return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), + new SpelAwareProxyProjectionFactory(), persistenceProvider); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + interface MyRepo extends Repository { + + MyRecord dtoProjection(); + } + + record Person(String id) { + + } + + record MyRecord(String foo, String bar) { + + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java new file mode 100644 index 000000000..425d68fba --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java @@ -0,0 +1,112 @@ +/* + * 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.assertj.core.api.Assertions.*; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * Unit tests for {@link DtoProjectionTransformerDelegate}. + * + * @author Mark Paluch + */ +class HqlDtoQueryTransformerUnitTests { + + JpaQueryMethod method = getMethod("dtoProjection"); + HqlSortedQueryTransformer transformer = new HqlSortedQueryTransformer(Sort.unsorted(), null, + method.getResultProcessor().getReturnedType()); + + @Test // GH-3076 + void shouldTranslateSingleProjectionToDto() { + + JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery("SELECT p from Person p"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); + } + + @Test // GH-3076 + void shouldRewriteQueriesWithSubselect() { + + JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser + .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "select new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); + } + + @Test // GH-3076 + void shouldNotTranslateConstructorExpressionQuery() { + + JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser + .parseQuery("SELECT NEW String(p) from Person p"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); + } + + @Test + void shouldTranslatePropertySelectionToDto() { + + JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser + .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); + } + + private JpaQueryMethod getMethod(String name, Class... parameterTypes) { + + try { + Method method = MyRepo.class.getMethod(name, parameterTypes); + PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; + + return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), + new SpelAwareProxyProjectionFactory(), persistenceProvider); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + interface MyRepo extends Repository { + + MyRecord dtoProjection(); + } + + record Person(String id) { + + } + + record MyRecord(String foo, String bar) { + + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java new file mode 100644 index 000000000..de6b22c90 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java @@ -0,0 +1,112 @@ +/* + * 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.assertj.core.api.Assertions.*; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * Unit tests for {@link DtoProjectionTransformerDelegate}. + * + * @author Mark Paluch + */ +class JpqlDtoQueryTransformerUnitTests { + + JpaQueryMethod method = getMethod("dtoProjection"); + JpqlSortedQueryTransformer transformer = new JpqlSortedQueryTransformer(Sort.unsorted(), null, + method.getResultProcessor().getReturnedType()); + + @Test // GH-3076 + void shouldTranslateSingleProjectionToDto() { + + JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery("SELECT p from Person p"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); + } + + @Test // GH-3076 + void shouldRewriteQueriesWithSubselect() { + + JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser + .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "select new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); + } + + @Test // GH-3076 + void shouldNotTranslateConstructorExpressionQuery() { + + JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser + .parseQuery("SELECT NEW String(p) from Person p"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); + } + + @Test + void shouldTranslatePropertySelectionToDto() { + + JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser + .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); + + QueryTokenStream visit = transformer.visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); + } + + private JpaQueryMethod getMethod(String name, Class... parameterTypes) { + + try { + Method method = MyRepo.class.getMethod(name, parameterTypes); + PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; + + return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), + new SpelAwareProxyProjectionFactory(), persistenceProvider); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + interface MyRepo extends Repository { + + MyRecord dtoProjection(); + } + + record Person(String id) { + + } + + record MyRecord(String foo, String bar) { + + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java index 7a9cf35d1..9522bf485 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java @@ -65,15 +65,8 @@ class NativeJpaQueryUnitTests { @Test // GH-3546 void shouldApplySorting() { - NativeJpaQuery query = getQuery(TestRepo.class, "find", Sort.class); - String sql = query.getSortedQueryString(Sort.by("foo", "bar")); - - assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc"); - } - - private NativeJpaQuery getQuery(Class repository, String method, Class... args) { - Method respositoryMethod = ReflectionUtils.findMethod(repository, method, args); - RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(repository); + Method respositoryMethod = ReflectionUtils.findMethod(TestRepo.class, "find", Sort.class); + RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(TestRepo.class); SpelAwareProxyProjectionFactory projectionFactory = mock(SpelAwareProxyProjectionFactory.class); QueryExtractor queryExtractor = mock(QueryExtractor.class); JpaQueryMethod queryMethod = new JpaQueryMethod(respositoryMethod, repositoryMetadata, projectionFactory, @@ -83,7 +76,9 @@ class NativeJpaQueryUnitTests { NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, annotation.value(), annotation.countQuery(), QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); - return query; + String sql = query.getSortedQueryString(Sort.by("foo", "bar"), queryMethod.getResultProcessor().getReturnedType()); + + assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc"); } interface TestRepo extends Repository { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 76896ece7..ac6a2ea9b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -721,6 +721,12 @@ public interface UserRepository extends JpaRepository, JpaSpecifi @Query("select u from User u where u.firstname >= (select Min(u0.firstname) from User u0)") List findProjectionBySubselect(); + @Query("select u from User u") + List findRecordProjection(); + + @Query("select u.firstname, u.lastname from User u") + List findMultiselectRecordProjection(); + Window findBy(OffsetScrollPosition position); interface RolesAndFirstname { @@ -747,4 +753,8 @@ public interface UserRepository extends JpaRepository, JpaSpecifi int getId(); } + record UserExcerpt(String firstname, String lastname) { + + } + } diff --git a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc index 00a6a8c8e..c55be8599 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc @@ -36,6 +36,41 @@ When using <> with JPQL, you must use (Note the usage of a FQDN for the DTO type!) This JPQL expression can be used in `@Query` annotations as well where you define any named queries. As a workaround you may use named queries with `ResultSetMapping` or the Hibernate-specific javadoc:{hibernatejavadocurl}org.hibernate.query.ResultListTransformer[] +===== DTO Projection JPQL Query Rewriting + +JPQL queries allow selection of the root object, individual properties, and DTO objects through constructor expressions. +Using a constructor expression can quickly add a lot of text to a query and make it difficult to read the actual query. +Spring Data JPA can support you with your JPQL queries by introducing constructor expressions for your convenience. + +Consider the following queries: + +.Projection Queries +==== +[source,java] +---- +interface UserRepository extends Repository { + + @Query("SELECT u FROM USER u") <1> + List findByLastname(String lastname); + + @Query("SELECT u.firstname, u.lastname FROM USER u") <2> + List findMultipleColumnsByLastname(String lastname); +} + +record UserDto(String firstname, String lastname){} +---- + +<1> Selection of the top-level entity. +This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM USER u`. +<2> Multi-select of `firstname` and `lastname` properties. +This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM USER u`. +==== + +Repository query methods that return a DTO projection type (a Java type outside the domain type hierarchy) are subject for query rewriting. +If an `@Query`-annotated query already uses constructor expressions, then Spring Data backs off and doesn't apply DTO constructor expression rewriting. + +Make sure that your DTO types provide an all-args constructor for the projection, otherwise the query will fail. + ==== Native Queries When using <>, their usage requires slightly more consideration depending on your :