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 ad0cafba9..f70210ff8 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 @@ -24,6 +24,7 @@ import jakarta.persistence.TupleElement; import jakarta.persistence.TypedQuery; import java.lang.reflect.Constructor; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.function.UnaryOperator; 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 193a255bb..95965b269 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 @@ -29,8 +29,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.jpa.repository.QueryRewriter; -import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -178,56 +176,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { return new NonProjectingReturnedType(returnedType); } - if (query.isDefaultProjection()) { - return returnedType; - } - String projectionToUse = query.<@Nullable String> doWithEnhancer(queryEnhancer -> { - - String alias = queryEnhancer.detectAlias(); - String projection = queryEnhancer.getProjection(); - - // we can handle single-column and no function projections here only - if (StringUtils.hasText(projection) && (projection.indexOf(',') != -1 || projection.indexOf('(') != -1)) { - return null; - } - - if (StringUtils.hasText(alias) && StringUtils.hasText(projection)) { - alias = alias.trim(); - projection = projection.trim(); - if (projection.startsWith(alias + ".")) { - projection = projection.substring(alias.length() + 1); - } - } - - int space = projection.indexOf(' '); - - if (space != -1) { - projection = projection.substring(0, space); - } - - return projection; - }); - - if (StringUtils.hasText(projectionToUse)) { - - Class propertyType; - - try { - PropertyPath from = PropertyPath.from(projectionToUse, getQueryMethod().getEntityInformation().getJavaType()); - propertyType = from.getLeafType(); - } catch (PropertyReferenceException ignored) { - propertyType = null; - } - - if (propertyType == null - || (returnedJavaType.isAssignableFrom(propertyType) || propertyType.isAssignableFrom(returnedJavaType))) { - knownProjections.put(returnedJavaType, false); - return new NonProjectingReturnedType(returnedType); - } else { - knownProjections.put(returnedJavaType, true); - } - } - + knownProjections.put(returnedJavaType, true); 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 index fd362c1e4..28de9ba65 100644 --- 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 @@ -17,6 +17,11 @@ package org.springframework.data.jpa.repository.query; import static org.springframework.data.jpa.repository.query.QueryTokens.*; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + import org.springframework.data.repository.query.ReturnedType; /** @@ -25,7 +30,8 @@ import org.springframework.data.repository.query.ReturnedType; * Query rewriting from a plain property/object selection towards constructor expression only works if either: * * * @author Mark Paluch @@ -34,42 +40,94 @@ import org.springframework.data.repository.query.ReturnedType; class DtoProjectionTransformerDelegate { private final ReturnedType returnedType; + private final boolean applyRewriting; + private final List selectItems = new ArrayList<>(); public DtoProjectionTransformerDelegate(ReturnedType returnedType) { this.returnedType = returnedType; + this.applyRewriting = returnedType.isProjecting() && !returnedType.getReturnedType().isInterface() + && returnedType.needsCustomConstruction(); } - public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) { - - if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface() - || !returnedType.needsCustomConstruction() || 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.getRequiredFirst().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(); + public boolean applyRewriting() { + return applyRewriting; } + + public boolean canRewrite() { + return applyRewriting() && !selectItems.isEmpty(); + } + + public void appendSelectItem(QueryTokenStream selectItem) { + + if (applyRewriting()) { + selectItems.add(new DetachedStream(selectItem)); + } + } + + public QueryTokenStream getRewrittenSelectionList() { + + if (canRewrite()) { + + QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.TOKEN_NEW); + builder.append(QueryTokens.token(returnedType.getReturnedType().getName())); + builder.append(QueryTokens.TOKEN_OPEN_PAREN); + + if (selectItems.size() == 1 && selectItems.get(0).size() == 1) { + + builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> { + + QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder(); + prop.appendInline(selectItems.get(0)); + prop.append(QueryTokens.TOKEN_DOT); + prop.append(QueryTokens.token(property)); + + return prop.build(); + }, QueryTokens.TOKEN_COMMA)); + } else { + builder.append(QueryTokenStream.concat(selectItems, Function.identity(), TOKEN_COMMA)); + } + + builder.append(TOKEN_CLOSE_PAREN); + + return builder.build(); + } + + return QueryTokenStream.empty(); + } + + private static class DetachedStream extends QueryRenderer { + + private final QueryTokenStream delegate; + + private DetachedStream(QueryTokenStream delegate) { + this.delegate = delegate; + } + + @Override + public boolean isExpression() { + return delegate.isExpression(); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + @Override + public String render() { + return delegate instanceof QueryRenderer ? ((QueryRenderer) delegate).render() : delegate.toString(); + } + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java index 2d8e27c16..7b2aa13fd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java @@ -17,9 +17,9 @@ package org.springframework.data.jpa.repository.query; import static org.springframework.data.jpa.repository.query.QueryTokens.*; -import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java index fa7fa5ec8..5f261a16b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java @@ -21,10 +21,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.springframework.data.jpa.repository.query.EqlParser.Range_variable_declarationContext; - import org.jspecify.annotations.Nullable; +import org.springframework.data.jpa.repository.query.EqlParser.Range_variable_declarationContext; + /** * {@link ParsedQueryIntrospector} for EQL queries. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index 51767d9c9..cadc135b7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java @@ -612,6 +612,15 @@ class EqlQueryRenderer extends EqlBaseVisitor { @Override public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { + QueryRendererBuilder builder = prepareSelectClause(ctx); + + builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); + + return builder; + } + + QueryRendererBuilder prepareSelectClause(EqlParser.Select_clauseContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.SELECT())); @@ -620,8 +629,6 @@ class EqlQueryRenderer extends EqlBaseVisitor { builder.append(QueryTokens.expression(ctx.DISTINCT())); } - builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); - return builder; } 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 30e9106d2..3c4bd92d7 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 @@ -19,9 +19,9 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; import java.util.List; -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; + +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.util.Assert; @@ -89,17 +89,53 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer { return super.visitSelect_clause(ctx); } - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder builder = prepareSelectClause(ctx); - builder.append(QueryTokens.expression(ctx.SELECT())); + QueryTokenStream selectItems = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); + if (dtoDelegate != null && dtoDelegate.canRewrite()) { + builder.append(dtoDelegate.getRewrittenSelectionList()); + } else { + builder.append(selectItems); } - QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); + return builder; + } - return builder.append(dtoDelegate.transformSelectionList(tokenStream)); + @Override + public QueryTokenStream visitSelect_item(EqlParser.Select_itemContext ctx) { + + QueryTokenStream tokens = super.visitSelect_item(ctx); + + if (ctx.result_variable() != null && !tokens.isEmpty()) { + transformerSupport.registerAlias(ctx.result_variable().getText()); + } + + return tokens; + } + + @Override + public QueryTokenStream visitSelect_expression(EqlParser.Select_expressionContext ctx) { + + QueryTokenStream selectItem = super.visitSelect_expression(ctx); + + if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.constructor_expression() == null) { + dtoDelegate.appendSelectItem(selectItem); + } + + return selectItem; + } + + @Override + public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { + + QueryTokenStream tokens = super.visitJoin(ctx); + + if (ctx.identification_variable() != null) { + transformerSupport.registerAlias(ctx.identification_variable().getText()); + } + + return tokens; } private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx) { @@ -129,28 +165,4 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer { } } - @Override - public QueryTokenStream visitSelect_item(EqlParser.Select_itemContext ctx) { - - QueryTokenStream tokens = super.visitSelect_item(ctx); - - if (ctx.result_variable() != null && !tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getRequiredLast()); - } - - return tokens; - } - - @Override - public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { - - QueryTokenStream tokens = super.visitJoin(ctx); - - if (!tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getRequiredLast()); - } - - return tokens; - } - } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java index e35b71258..a4bd3af55 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java @@ -107,7 +107,7 @@ class HqlCountQueryTransformer extends HqlQueryRenderer { if (ctx.fromClause() != null) { builder.appendExpression(visit(ctx.fromClause())); - if(primaryFromAlias == null) { + if (primaryFromAlias == null) { builder.append(TOKEN_AS); builder.append(TOKEN_DOUBLE_UNDERSCORE); } @@ -150,7 +150,6 @@ class HqlCountQueryTransformer extends HqlQueryRenderer { return builder; } - @Override public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java index 1d73d078f..e1ed4997f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java @@ -44,8 +44,8 @@ import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; import org.hibernate.query.criteria.HibernateCriteriaBuilder; - import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.mapping.PropertyPath; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java index 01557b33d..61f3fcf21 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java @@ -885,7 +885,7 @@ class HqlQueryRenderer extends HqlBaseVisitor { builder.appendExpression(visit(ctx.variable())); } - return builder; + return builder.toInline(); } @Override 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 175e918c8..99a677a41 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 @@ -93,13 +93,25 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer { QueryTokenStream tokenStream = super.visitSelectionList(ctx); - if (dtoDelegate != null && !isSubquery(ctx)) { - return dtoDelegate.transformSelectionList(tokenStream); + if (dtoDelegate != null && dtoDelegate.canRewrite() && !isSubquery(ctx)) { + return dtoDelegate.getRewrittenSelectionList(); } return tokenStream; } + @Override + public QueryTokenStream visitSelectExpression(HqlParser.SelectExpressionContext ctx) { + + QueryTokenStream selectItem = super.visitSelectExpression(ctx); + + if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.instantiation() == null && !isSubquery(ctx)) { + dtoDelegate.appendSelectItem(QueryRenderer.ofExpression(selectItem)); + } + + return selectItem; + } + @Override public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) { @@ -123,7 +135,7 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer { return tokens; } - + @Override public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext ctx) { @@ -135,7 +147,6 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer { return tokens; } - @Override public QueryTokenStream visitVariable(HqlParser.VariableContext ctx) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index e317528e8..124df5034 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -945,7 +945,8 @@ public final class JpqlQueryBuilder { */ public String getAlias(Origin source) { - return aliases.computeIfAbsent(source, it -> JpqlQueryBuilder.getAlias(source.getName(), s -> !aliases.containsValue(s), () -> "join_" + (counter++))); + return aliases.computeIfAbsent(source, it -> JpqlQueryBuilder.getAlias(source.getName(), + s -> !aliases.containsValue(s), () -> "join_" + (counter++))); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index 03b87cdd3..3167e0951 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java @@ -594,6 +594,15 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { @Override public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) { + QueryRendererBuilder builder = prepareSelectClause(ctx); + + builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); + + return builder; + } + + QueryRendererBuilder prepareSelectClause(JpqlParser.Select_clauseContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.SELECT())); @@ -602,8 +611,6 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { builder.append(QueryTokens.expression(ctx.DISTINCT())); } - builder.append(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); - return builder; } @@ -2219,7 +2226,7 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { } else if (ctx.type_literal() != null) { return visit(ctx.type_literal()); } else if (ctx.f != null) { - return QueryTokenStream.ofToken(ctx.f); + return QueryRenderer.from(QueryTokens.expression(ctx.f)); } else { return QueryTokenStream.empty(); } 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 654fb7df8..807140c5b 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 @@ -19,9 +19,9 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; import java.util.List; -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; + +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.util.Assert; @@ -72,7 +72,7 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { builder.appendExpression(visit(ctx.having_clause())); } - if(ctx.set_fuction() != null) { + if (ctx.set_fuction() != null) { builder.appendExpression(visit(ctx.set_fuction())); } else { doVisitOrderBy(builder, ctx); @@ -88,17 +88,53 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { return super.visitSelect_clause(ctx); } - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder builder = prepareSelectClause(ctx); - builder.append(QueryTokens.expression(ctx.SELECT())); + QueryTokenStream selectItems = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); + if (dtoDelegate != null && dtoDelegate.canRewrite()) { + builder.append(dtoDelegate.getRewrittenSelectionList()); + } else { + builder.append(selectItems); } - QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); + return builder; + } - return builder.append(dtoDelegate.transformSelectionList(tokenStream)); + @Override + public QueryTokenStream visitSelect_item(JpqlParser.Select_itemContext ctx) { + + QueryTokenStream tokens = super.visitSelect_item(ctx); + + if (ctx.result_variable() != null && !tokens.isEmpty()) { + transformerSupport.registerAlias(ctx.result_variable().getText()); + } + + return tokens; + } + + @Override + public QueryTokenStream visitSelect_expression(JpqlParser.Select_expressionContext ctx) { + + QueryTokenStream selectItem = super.visitSelect_expression(ctx); + + if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.constructor_expression() == null) { + dtoDelegate.appendSelectItem(selectItem); + } + + return selectItem; + } + + @Override + public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { + + QueryTokenStream tokens = super.visitJoin(ctx); + + if (ctx.identification_variable() != null) { + transformerSupport.registerAlias(ctx.identification_variable().getText()); + } + + return tokens; } private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) { @@ -127,29 +163,4 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { } } } - - @Override - public QueryTokenStream visitSelect_item(JpqlParser.Select_itemContext ctx) { - - QueryTokenStream tokens = super.visitSelect_item(ctx); - - if (ctx.result_variable() != null && !tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getRequiredLast()); - } - - return tokens; - } - - @Override - public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { - - QueryTokenStream tokens = super.visitJoin(ctx); - - if (!tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getRequiredLast()); - } - - return tokens; - } - } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java index 5bd645a11..402fca8f7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java @@ -22,10 +22,10 @@ import java.util.Iterator; import java.util.List; import java.util.stream.Stream; -import org.springframework.util.CompositeIterator; - import org.jspecify.annotations.Nullable; +import org.springframework.util.CompositeIterator; + /** * Abstraction to encapsulate query expressions and render a query. *

@@ -622,6 +622,9 @@ abstract class QueryRenderer implements QueryTokenStream { return current; } + public QueryRenderer toInline() { + return new InlineRenderer(current); + } } private static class InlineRenderer extends QueryRenderer { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java index 31d4a44d4..9ad2fe8f3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java @@ -40,8 +40,4 @@ class EclipseLinkUserRepositoryFinderTests extends UserRepositoryFinderTests { @Override void shouldProjectWithKeysetScrolling() {} - @Disabled - @Override - void rawMapProjectionWithEntityAndAggregatedValue() {} - } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryProjectionTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryProjectionTests.java new file mode 100644 index 000000000..d4d6e2a14 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryProjectionTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2011-2025 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; + +import org.junit.jupiter.api.Disabled; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Oliver Gierke + * @author Greg Turnquist + */ +@ContextConfiguration("classpath:eclipselink-h2.xml") +class EclipseLinkUserRepositoryProjectionTests extends UserRepositoryProjectionTests { + + @Disabled + @Override + void rawMapProjectionWithEntityAndAggregatedValue() {} + +} 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 46721b1df..2c73a6480 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 @@ -22,15 +22,11 @@ import jakarta.persistence.EntityManager; import java.util.Arrays; import java.util.List; -import java.util.Map; -import org.assertj.core.data.Offset; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -42,18 +38,12 @@ import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; -import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.sample.RoleRepository; import org.springframework.data.jpa.repository.sample.UserRepository; -import org.springframework.data.jpa.repository.sample.UserRepository.IdOnly; import org.springframework.data.jpa.repository.sample.UserRepository.NameOnly; -import org.springframework.data.jpa.repository.sample.UserRepository.RolesAndFirstname; -import org.springframework.data.jpa.repository.sample.UserRepository.UserExcerpt; -import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountDtoProjection; -import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountInterfaceProjection; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -350,29 +340,6 @@ class UserRepositoryFinderTests { .containsExactlyInAnyOrder(dave, oliver); } - @Test // DATAJPA-974, GH-2815 - void executesQueryWithProjectionContainingReferenceToPluralAttribute() { - - List rolesAndFirstnameBy = userRepository.findRolesAndFirstnameBy(); - - assertThat(rolesAndFirstnameBy).isNotNull(); - - for (RolesAndFirstname rolesAndFirstname : rolesAndFirstnameBy) { - assertThat(rolesAndFirstname.getFirstname()).isNotNull(); - assertThat(rolesAndFirstname.getRoles()).isNotNull(); - } - } - - @Test // GH-2815 - void executesQueryWithProjectionThroughStringQuery() { - - List ids = userRepository.findIdOnly(); - - assertThat(ids).isNotNull(); - - assertThat(ids).extracting(IdOnly::getId).doesNotContainNull(); - } - @Test // DATAJPA-1023, DATACMNS-959 @Transactional(propagation = Propagation.NOT_SUPPORTED) void rejectsStreamExecutionIfNoSurroundingTransactionActive() { @@ -381,22 +348,6 @@ class UserRepositoryFinderTests { .isThrownBy(() -> userRepository.findAllByCustomQueryAndStream()); } - @Test // DATAJPA-1334 - void executesNamedQueryWithConstructorExpression() { - userRepository.findByNamedQueryWithConstructorExpression(); - } - - @Test // DATAJPA-1713, GH-2008 - void selectProjectionWithSubselect() { - - List dtos = userRepository.findProjectionBySubselect(); - - assertThat(dtos).flatExtracting(UserRepository.NameOnly::getFirstname) // - .containsExactly("Dave", "Carter", "Oliver August"); - assertThat(dtos).flatExtracting(UserRepository.NameOnly::getLastname) // - .containsExactly("Matthews", "Beauford", "Matthews"); - } - @Test // GH-3675 void findBySimplePropertyUsingMixedNullNonNullArgument() { @@ -415,114 +366,6 @@ class UserRepositoryFinderTests { assertThat(result).containsExactly(carter); } - @Test // GH-3076 - void dtoProjectionShouldApplyConstructorExpressionRewriting() { - - List dtos = userRepository.findRecordProjection(); - - assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) // - .contains("Dave", "Carter", "Oliver August"); - - dtos = userRepository.findRecordProjectionWithFunctions(); - - assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::lastname) // - .contains("matthews", "beauford"); - } - - @Test // GH-3076 - void dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() { - - List dtos = userRepository.findMultiselectRecordProjection(); - - assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) // - .contains("Dave", "Carter", "Oliver August"); - } - - @Test // GH-3076 - void dynamicDtoProjection() { - - List dtos = userRepository.findRecordProjection(UserExcerpt.class); - - assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) // - .contains("Dave", "Carter", "Oliver August"); - } - - @Test // GH-3862 - void shouldNotRewritePrimitiveSelectionToDtoProjection() { - - oliver.setAge(28); - em.persist(oliver); - - assertThat(userRepository.findAgeByAnnotatedQuery(oliver.getEmailAddress())).contains(28); - } - - @Test // GH-3862 - void shouldNotRewritePropertySelectionToDtoProjection() { - - Address address = new Address("DE", "Dresden", "some street", "12345"); - dave.setAddress(address); - userRepository.save(dave); - em.flush(); - em.clear(); - - assertThat(userRepository.findAddressByAnnotatedQuery(dave.getEmailAddress())).contains(address); - assertThat(userRepository.findCityByAnnotatedQuery(dave.getEmailAddress())).contains("Dresden"); - assertThat(userRepository.findRolesByAnnotatedQuery(dave.getEmailAddress())).contains(singer); - } - - @Test // GH-3076 - void dtoProjectionWithEntityAndAggregatedValue() { - - Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, - oliver.getFirstname(), oliver); - - assertThat(userRepository.dtoProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { - assertThat(projection.user()).isIn(musicians.values()); - assertThat(projection.roleCount()).isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(), - Offset.offset(0L)); - }); - } - - @Test // GH-3076 - void interfaceProjectionWithEntityAndAggregatedValue() { - - Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, - oliver.getFirstname(), oliver); - - assertThat(userRepository.interfaceProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { - assertThat(projection.getUser()).isIn(musicians.values()); - assertThat(projection.getRoleCount()) - .isCloseTo(musicians.get(projection.getUser().getFirstname()).getRoles().size(), Offset.offset(0L)); - }); - } - - @Test // GH-3076 - void rawMapProjectionWithEntityAndAggregatedValue() { - - Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, - oliver.getFirstname(), oliver); - - assertThat(userRepository.rawMapProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { - assertThat(projection.get("user")).isIn(musicians.values()); - assertThat(projection).containsKey("roleCount"); - }); - } - - @Test // GH-3076 - void dtoProjectionWithEntityAndAggregatedValueWithPageable() { - - Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, - oliver.getFirstname(), oliver); - - assertThat( - userRepository.dtoProjectionEntityAndAggregatedValue(PageRequest.of(0, 10).withSort(Sort.by("firstname")))) - .allSatisfy(projection -> { - assertThat(projection.user()).isIn(musicians.values()); - assertThat(projection.roleCount()) - .isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(), Offset.offset(0L)); - }); - } - @Test // GH-3857 void shouldApplyParameterNames() { @@ -531,12 +374,4 @@ class UserRepositoryFinderTests { oliver.getLastname())).hasSize(2); } - @ParameterizedTest // GH-3076 - @ValueSource(classes = { UserRoleCountDtoProjection.class, UserRoleCountInterfaceProjection.class }) - void dynamicProjectionWithEntityAndAggregated(Class resultType) { - - assertThat(userRepository.findMultiselectRecordDynamicProjection(resultType)).hasSize(3) - .hasOnlyElementsOfType(resultType); - } - } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryProjectionTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryProjectionTests.java new file mode 100644 index 000000000..8771939ac --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryProjectionTests.java @@ -0,0 +1,286 @@ +/* + * Copyright 2008-2025 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; + +import static org.assertj.core.api.Assertions.*; + +import jakarta.persistence.EntityManager; + +import java.util.List; +import java.util.Map; + +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.Address; +import org.springframework.data.jpa.domain.sample.Role; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.sample.RoleRepository; +import org.springframework.data.jpa.repository.sample.UserRepository; +import org.springframework.data.jpa.repository.sample.UserRepository.IdOnly; +import org.springframework.data.jpa.repository.sample.UserRepository.NameOnly; +import org.springframework.data.jpa.repository.sample.UserRepository.RolesAndFirstname; +import org.springframework.data.jpa.repository.sample.UserRepository.UserExcerpt; +import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountDtoProjection; +import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountInterfaceProjection; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration test for executing projecting query methods. + * + * @author Oliver Gierke + * @author Krzysztof Krason + * @author Greg Turnquist + * @author Mark Paluch + * @author Christoph Strobl + * @see QueryLookupStrategy + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(locations = "classpath:config/namespace-application-context-h2.xml") +@Transactional +class UserRepositoryProjectionTests { + + @Autowired UserRepository userRepository; + @Autowired RoleRepository roleRepository; + @Autowired EntityManager em; + + PersistenceProvider provider; + + private User dave; + private User carter; + private User oliver; + private Role drummer; + private Role guitarist; + private Role singer; + + @BeforeEach + void setUp() { + + drummer = roleRepository.save(new Role("DRUMMER")); + guitarist = roleRepository.save(new Role("GUITARIST")); + singer = roleRepository.save(new Role("SINGER")); + + dave = userRepository.save(new User("Dave", "Matthews", "dave@dmband.com", singer)); + carter = userRepository.save(new User("Carter", "Beauford", "carter@dmband.com", singer, drummer)); + oliver = userRepository.save(new User("Oliver August", "Matthews", "oliver@dmband.com")); + + provider = PersistenceProvider.fromEntityManager(em); + } + + @AfterEach + void clearUp() { + + userRepository.deleteAll(); + roleRepository.deleteAll(); + } + + @Test // DATAJPA-974, GH-2815 + void executesQueryWithProjectionContainingReferenceToPluralAttribute() { + + List rolesAndFirstnameBy = userRepository.findRolesAndFirstnameBy(); + + assertThat(rolesAndFirstnameBy).isNotNull(); + + for (RolesAndFirstname rolesAndFirstname : rolesAndFirstnameBy) { + assertThat(rolesAndFirstname.getFirstname()).isNotNull(); + assertThat(rolesAndFirstname.getRoles()).isNotNull(); + } + } + + @Test // GH-2815 + void executesQueryWithProjectionThroughStringQuery() { + + List ids = userRepository.findIdOnly(); + + assertThat(ids).isNotNull(); + + assertThat(ids).extracting(IdOnly::getId).doesNotContainNull(); + } + + @Test // DATAJPA-1334 + void executesNamedQueryWithConstructorExpression() { + userRepository.findByNamedQueryWithConstructorExpression(); + } + + @Test // DATAJPA-1713, GH-2008 + void selectProjectionWithSubselect() { + + List dtos = userRepository.findProjectionBySubselect(); + + assertThat(dtos).flatExtracting(NameOnly::getFirstname) // + .containsExactly("Dave", "Carter", "Oliver August"); + assertThat(dtos).flatExtracting(NameOnly::getLastname) // + .containsExactly("Matthews", "Beauford", "Matthews"); + } + + @Test // GH-3076 + void dtoProjectionShouldApplyConstructorExpressionRewriting() { + + List dtos = userRepository.findRecordProjection(); + + assertThat(dtos).flatExtracting(UserExcerpt::firstname) // + .contains("Dave", "Carter", "Oliver August"); + + dtos = userRepository.findRecordProjectionWithFunctions(); + + assertThat(dtos).flatExtracting(UserExcerpt::lastname) // + .contains("matthews", "beauford"); + } + + @Test // GH-3895 + void stringProjectionShouldNotApplyConstructorExpressionRewriting() { + + List names = userRepository.findStringProjection(); + + assertThat(names) // + .contains("Dave", "Carter", "Oliver August"); + } + + @Test // GH-3895 + void objectArrayProjectionShouldNotApplyConstructorExpressionRewriting() { + + List names = userRepository.findObjectArrayProjectionWithFunctions(); + + assertThat(names) // + .contains(new String[] { "Dave", "matthews" }); + } + + @Test // GH-3076 + void dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() { + + List dtos = userRepository.findMultiselectRecordProjection(); + + assertThat(dtos).flatExtracting(UserExcerpt::firstname) // + .contains("Dave", "Carter", "Oliver August"); + } + + @Test // GH-3895 + void dtoMultiselectProjectionShouldApplyConstructorExpressionRewritingForJoin() { + + dave.setAddress(new Address("US", "Albuquerque", "some street", "12345")); + + List dtos = userRepository.findAddressProjection(); + + assertThat(dtos).flatExtracting(UserRepository.AddressDto::city) // + .contains("Albuquerque"); + } + + @Test // GH-3076 + void dynamicDtoProjection() { + + List dtos = userRepository.findRecordProjection(UserExcerpt.class); + + assertThat(dtos).flatExtracting(UserExcerpt::firstname) // + .contains("Dave", "Carter", "Oliver August"); + } + + @Test // GH-3862 + void shouldNotRewritePrimitiveSelectionToDtoProjection() { + + oliver.setAge(28); + em.persist(oliver); + + assertThat(userRepository.findAgeByAnnotatedQuery(oliver.getEmailAddress())).contains(28); + } + + @Test // GH-3862 + void shouldNotRewritePropertySelectionToDtoProjection() { + + Address address = new Address("DE", "Dresden", "some street", "12345"); + dave.setAddress(address); + userRepository.save(dave); + em.flush(); + em.clear(); + + assertThat(userRepository.findAddressByAnnotatedQuery(dave.getEmailAddress())).contains(address); + assertThat(userRepository.findCityByAnnotatedQuery(dave.getEmailAddress())).contains("Dresden"); + assertThat(userRepository.findRolesByAnnotatedQuery(dave.getEmailAddress())).contains(singer); + } + + @Test // GH-3076 + void dtoProjectionWithEntityAndAggregatedValue() { + + Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, + oliver.getFirstname(), oliver); + + assertThat(userRepository.dtoProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { + assertThat(projection.user()).isIn(musicians.values()); + assertThat(projection.roleCount()).isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(), + Offset.offset(0L)); + }); + } + + @Test // GH-3076 + void interfaceProjectionWithEntityAndAggregatedValue() { + + Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, + oliver.getFirstname(), oliver); + + assertThat(userRepository.interfaceProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { + assertThat(projection.getUser()).isIn(musicians.values()); + assertThat(projection.getRoleCount()) + .isCloseTo(musicians.get(projection.getUser().getFirstname()).getRoles().size(), Offset.offset(0L)); + }); + } + + @Test // GH-3076 + void rawMapProjectionWithEntityAndAggregatedValue() { + + Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, + oliver.getFirstname(), oliver); + + assertThat(userRepository.rawMapProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { + assertThat(projection.get("user")).isIn(musicians.values()); + assertThat(projection).containsKey("roleCount"); + }); + } + + @Test // GH-3076 + void dtoProjectionWithEntityAndAggregatedValueWithPageable() { + + Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, + oliver.getFirstname(), oliver); + + assertThat( + userRepository.dtoProjectionEntityAndAggregatedValue(PageRequest.of(0, 10).withSort(Sort.by("firstname")))) + .allSatisfy(projection -> { + assertThat(projection.user()).isIn(musicians.values()); + assertThat(projection.roleCount()) + .isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(), Offset.offset(0L)); + }); + } + + @ParameterizedTest // GH-3076 + @ValueSource(classes = { UserRoleCountDtoProjection.class, UserRoleCountInterfaceProjection.class }) + void dynamicProjectionWithEntityAndAggregated(Class resultType) { + + assertThat(userRepository.findMultiselectRecordDynamicProjection(resultType)).hasSize(3) + .hasOnlyElementsOfType(resultType); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java index e72ded4fc..fcede5da4 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java @@ -38,7 +38,7 @@ abstract class AbstractDtoQueryTransformerUnitTests

... parameterTypes) { try { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index 43022e6bb..47949f1a6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java @@ -60,7 +60,6 @@ import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.data.util.TypeInformation; /** * Unit test for {@link SimpleJpaQuery}. @@ -274,10 +273,7 @@ class SimpleJpaQueryUnitTests { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("selectWithJoin")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); assertThat(queryString).startsWith("SELECT cd FROM CampaignDeal cd"); } @@ -288,41 +284,34 @@ class SimpleJpaQueryUnitTests { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("selectWithJoin")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); assertThat(queryString).startsWith( "SELECT new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(cd.name)"); } @Test // GH-3895 - void doesNotRewriteQueryForUnknownProperty() throws Exception { + void rewritesQueryForUnknownProperty() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("projectWithUnknownPaths")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); - assertThat(queryString).startsWith("select u.unknown from User u"); + assertThat(queryString).startsWith( + "select new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(u.unknown)"); } @Test // GH-3895 - void doesNotRewriteQueryForJoinPath() throws Exception { + void rewritesQueryForJoinPath() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("projectWithJoinPaths")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); - assertThat(queryString).startsWith("select r.name from User u LEFT JOIN FETCH u.roles r"); + assertThat(queryString).startsWith( + "select new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(r.name) from User u LEFT JOIN FETCH u.roles r"); } @Test // DATAJPA-1307 @@ -372,6 +361,13 @@ class SimpleJpaQueryUnitTests { countQueryString == null ? null : countQueryString.orElse(queryMethod.getDeclaredCountQuery())); } + private String createQuery(AbstractStringBasedJpaQuery jpaQuery) { + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + return jpaQuery.getSortedQuery(Sort.unsorted(), jpaQuery.getReturnedType(processor)).getQueryString(); + } + interface SampleRepository extends Repository { @Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true) 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 2fc34657f..2d2c46bb8 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 @@ -718,9 +718,18 @@ public interface UserRepository extends JpaRepository, JpaSpecifi @Query("select u from User u") List findRecordProjection(); - @Query("select u.firstname, LOWER(u.lastname) from User u") + @Query("select u.firstname as fn, LOWER(u.lastname) as lastname from User u") List findRecordProjectionWithFunctions(); + @Query("select u.firstname from User u") + List findStringProjection(); + + @Query("select u.firstname, LOWER(u.lastname) from User u") + List findObjectArrayProjectionWithFunctions(); + + @Query("select u.address from User u") + List findAddressProjection(); + @Query("select u from User u") List findRecordProjection(Class projectionType); @@ -807,6 +816,12 @@ public interface UserRepository extends JpaRepository, JpaSpecifi } + record AddressDto(String country, String city) { + public AddressDto(Address address) { + this(address != null ? address.getCountry() : null, address != null ? address.getCity() : null); + } + } + record UserRoleCountDtoProjection(User user, Long roleCount) { }