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 fb9821c18..45d5ed13b 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.Arrays; import java.util.Collection; @@ -474,7 +475,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { * * @author Jens Schauder */ - private static class TupleBackedMap implements Map { + private static class TupleBackedMap extends AbstractMap implements Map { private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified"; 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 2f2cd797d..f447716c1 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 @@ -27,8 +27,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; @@ -166,52 +164,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { return new NonProjectingReturnedType(returnedType); } - if (query.isDefaultProjection()) { - return returnedType; - } - - String alias = query.getAlias(); - String projection = query.getProjection(); - - // we can handle single-column and no function projections here only - if (StringUtils.hasText(projection) && (projection.indexOf(',') != -1 || projection.indexOf('(') != -1)) { - return returnedType; - } - - if (StringUtils.hasText(alias) && StringUtils.hasText(projection)) { - alias = alias.trim(); - projection = projection.trim(); - if (projection.startsWith(alias + ".")) { - projection = projection.substring(alias.length() + 1); - } - } - - if (StringUtils.hasText(projection)) { - - int space = projection.indexOf(' '); - - if (space != -1) { - projection = projection.substring(0, space); - } - - Class propertyType; - - try { - PropertyPath from = PropertyPath.from(projection, 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 4bed2ab68..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: *
    *
  • The query selects its primary alias ({@code SELECT p FROM Person p})
  • - *
  • The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p})
  • + *
  • The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p}, + * {@code SELECT COUNT(p.foo), p.bar AS bar FROM Person p})
  • *
* * @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.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(); + 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/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index 22b264cc1..1da19ca21 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 @@ -292,8 +292,7 @@ class EqlQueryRenderer extends EqlBaseVisitor { } @Override - public QueryTokenStream visitJoin_association_path_expression( - EqlParser.Join_association_path_expressionContext ctx) { + public QueryTokenStream visitJoin_association_path_expression(EqlParser.Join_association_path_expressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -451,8 +450,7 @@ class EqlQueryRenderer extends EqlBaseVisitor { } @Override - public QueryTokenStream visitGeneral_identification_variable( - EqlParser.General_identification_variableContext ctx) { + public QueryTokenStream visitGeneral_identification_variable(EqlParser.General_identification_variableContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -641,6 +639,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())); @@ -649,8 +656,6 @@ class EqlQueryRenderer extends EqlBaseVisitor { builder.append(QueryTokens.expression(ctx.DISTINCT())); } - builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); - return builder; } @@ -2448,8 +2453,7 @@ class EqlQueryRenderer extends EqlBaseVisitor { } @Override - public QueryTokenStream visitCharacter_valued_input_parameter( - EqlParser.Character_valued_input_parameterContext ctx) { + public QueryTokenStream visitCharacter_valued_input_parameter(EqlParser.Character_valued_input_parameterContext ctx) { if (ctx.CHARACTER() != null) { return QueryRendererBuilder.from(QueryTokens.expression(ctx.CHARACTER())); 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 e54402475..e54979e00 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 @@ -91,17 +91,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, Sort sort) { @@ -131,28 +167,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.getLast()); - } - - return tokens; - } - - @Override - public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { - - QueryTokenStream tokens = super.visitJoin(ctx); - - if (!tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getLast()); - } - - 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 b6a90c559..85e4d1d24 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 @@ -103,13 +103,12 @@ 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); } } - if (ctx.whereClause() != null) { builder.appendExpression(visit(ctx.whereClause())); } 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 6b1bf850e..a6936ddbb 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 @@ -830,7 +830,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 eadee9496..43083634f 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 @@ -91,13 +91,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.expression(selectItem)); + } + + return selectItem; + } + @Override public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) { 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 41ca18396..fae722d52 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 @@ -572,6 +572,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())); @@ -580,8 +589,6 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { builder.append(QueryTokens.expression(ctx.DISTINCT())); } - builder.append(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); - return builder; } @@ -2114,7 +2121,7 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { if (ctx.IDENTIFICATION_VARIABLE() != null) { return QueryRenderer.from(QueryTokens.expression(ctx.IDENTIFICATION_VARIABLE())); } else if (ctx.f != null) { - return QueryRenderer.from(QueryTokens.token(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 41d0661d2..cca1ae198 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 @@ -83,17 +83,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) { @@ -122,28 +158,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.getLast()); - } - - return tokens; - } - - @Override - public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { - - QueryTokenStream tokens = super.visitJoin(ctx); - - if (!tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getLast()); - } - - return tokens; - } } 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 8593c1ed3..75cfa39d8 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 @@ -36,8 +36,4 @@ class EclipseLinkUserRepositoryFinderTests extends UserRepositoryFinderTests { @Override void executesInKeywordForPageCorrectly() {} - @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 7eb4e4cc6..d18081e28 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,120 +366,4 @@ 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)); - }); - } - - @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 38f109dbd..e2934c84d 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 @@ -273,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"); } @@ -287,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 @@ -371,6 +361,13 @@ class SimpleJpaQueryUnitTests { countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery())); } + private String createQuery(AbstractStringBasedJpaQuery jpaQuery) { + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + return jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + } + 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 88efe091e..c7ec3362f 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 @@ -724,9 +724,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) { }