Refine DTO projection rewriting.

We now consider dropping aliases (count(foo) as foo), support subselects and capture individual select items to avoid contextual information loss.

Also, added a series of tests to cover edgecases.

See #3895
This commit is contained in:
Mark Paluch
2025-06-03 11:16:40 +02:00
parent 205912ccc2
commit ea9e6e15bb
22 changed files with 617 additions and 364 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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:
* <ul>
* <li>The query selects its primary alias ({@code SELECT p FROM Person p})</li>
* <li>The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p})</li>
* <li>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})</li>
* </ul>
*
* @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<QueryTokenStream> 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<QueryToken> iterator() {
return delegate.iterator();
}
@Override
public String render() {
return delegate instanceof QueryRenderer ? ((QueryRenderer) delegate).render() : delegate.toString();
}
}
}

View File

@@ -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;
/**

View File

@@ -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.
*

View File

@@ -612,6 +612,15 @@ class EqlQueryRenderer extends EqlBaseVisitor<QueryTokenStream> {
@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<QueryTokenStream> {
builder.append(QueryTokens.expression(ctx.DISTINCT()));
}
builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA));
return builder;
}

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -885,7 +885,7 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryTokenStream> {
builder.appendExpression(visit(ctx.variable()));
}
return builder;
return builder.toInline();
}
@Override

View File

@@ -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) {

View File

@@ -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++)));
}
/**

View File

@@ -594,6 +594,15 @@ class JpqlQueryRenderer extends JpqlBaseVisitor<QueryTokenStream> {
@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<QueryTokenStream> {
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<QueryTokenStream> {
} 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();
}

View File

@@ -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;
}
}

View File

@@ -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.
* <p>
@@ -622,6 +622,9 @@ abstract class QueryRenderer implements QueryTokenStream {
return current;
}
public QueryRenderer toInline() {
return new InlineRenderer(current);
}
}
private static class InlineRenderer extends QueryRenderer {

View File

@@ -40,8 +40,4 @@ class EclipseLinkUserRepositoryFinderTests extends UserRepositoryFinderTests {
@Override
void shouldProjectWithKeysetScrolling() {}
@Disabled
@Override
void rawMapProjectionWithEntityAndAggregatedValue() {}
}

View File

@@ -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() {}
}

View File

@@ -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<RolesAndFirstname> rolesAndFirstnameBy = userRepository.findRolesAndFirstnameBy();
assertThat(rolesAndFirstnameBy).isNotNull();
for (RolesAndFirstname rolesAndFirstname : rolesAndFirstnameBy) {
assertThat(rolesAndFirstname.getFirstname()).isNotNull();
assertThat(rolesAndFirstname.getRoles()).isNotNull();
}
}
@Test // GH-2815
void executesQueryWithProjectionThroughStringQuery() {
List<IdOnly> 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<UserRepository.NameOnly> 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<UserExcerpt> 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<UserExcerpt> dtos = userRepository.findMultiselectRecordProjection();
assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) //
.contains("Dave", "Carter", "Oliver August");
}
@Test // GH-3076
void dynamicDtoProjection() {
List<UserExcerpt> 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<String, User> 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<String, User> 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<String, User> 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<String, User> 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 })
<T> void dynamicProjectionWithEntityAndAggregated(Class<T> resultType) {
assertThat(userRepository.findMultiselectRecordDynamicProjection(resultType)).hasSize(3)
.hasOnlyElementsOfType(resultType);
}
}

View File

@@ -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<RolesAndFirstname> rolesAndFirstnameBy = userRepository.findRolesAndFirstnameBy();
assertThat(rolesAndFirstnameBy).isNotNull();
for (RolesAndFirstname rolesAndFirstname : rolesAndFirstnameBy) {
assertThat(rolesAndFirstname.getFirstname()).isNotNull();
assertThat(rolesAndFirstname.getRoles()).isNotNull();
}
}
@Test // GH-2815
void executesQueryWithProjectionThroughStringQuery() {
List<IdOnly> 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<NameOnly> 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<UserExcerpt> 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<String> names = userRepository.findStringProjection();
assertThat(names) //
.contains("Dave", "Carter", "Oliver August");
}
@Test // GH-3895
void objectArrayProjectionShouldNotApplyConstructorExpressionRewriting() {
List<Object[]> names = userRepository.findObjectArrayProjectionWithFunctions();
assertThat(names) //
.contains(new String[] { "Dave", "matthews" });
}
@Test // GH-3076
void dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() {
List<UserExcerpt> 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<UserRepository.AddressDto> dtos = userRepository.findAddressProjection();
assertThat(dtos).flatExtracting(UserRepository.AddressDto::city) //
.contains("Albuquerque");
}
@Test // GH-3076
void dynamicDtoProjection() {
List<UserExcerpt> 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<String, User> 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<String, User> 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<String, User> 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<String, User> 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 })
<T> void dynamicProjectionWithEntityAndAggregated(Class<T> resultType) {
assertThat(userRepository.findMultiselectRecordDynamicProjection(resultType)).hasSize(3)
.hasOnlyElementsOfType(resultType);
}
}

View File

@@ -38,7 +38,7 @@ abstract class AbstractDtoQueryTransformerUnitTests<P extends JpaQueryEnhancer<?
JpaQueryMethod method = getMethod("dtoProjection");
@Test // GH-3076
void shouldTranslateSingleProjectionToDto() {
void shouldRewritePrimarySelectionToConstructorExpressionWithProperties() {
P parser = parse("SELECT p from Person p");
@@ -48,6 +48,17 @@ abstract class AbstractDtoQueryTransformerUnitTests<P extends JpaQueryEnhancer<?
"SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p");
}
@Test // GH-3076, GH-3895
void shouldRewriteSelectionToConstructorExpression() {
P parser = parse("SELECT p.name from Person p");
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.name) from Person p");
}
@Test // GH-3076
void shouldRewriteQueriesWithSubselect() {
@@ -80,7 +91,7 @@ abstract class AbstractDtoQueryTransformerUnitTests<P extends JpaQueryEnhancer<?
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW com.foo(p) from Person p");
}
@Test
@Test // GH-3076
void shouldTranslatePropertySelectionToDto() {
P parser = parse("SELECT p.foo, p.bar, sum(p.age) from Person p");
@@ -91,6 +102,29 @@ abstract class AbstractDtoQueryTransformerUnitTests<P extends JpaQueryEnhancer<?
"SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p");
}
@Test // GH-3895
void shouldStripAliasesFromDtoProjection() {
P parser = parse("SELECT sum(p.age) As age, p.foo as foo, p.bar AS bar from Person p");
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(sum(p.age), p.foo, p.bar) from Person p");
}
@Test // GH-3895
void shouldStripAliasesFromDtoProjectionWithSubquery() {
P parser = parse(
"SELECT p.foo as foo, p.bar AS bar, cast(p.age as INTEGER) As age, (SELECT b.foo FROM Bar AS b) from Person p");
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, cast(p.age as INTEGER), (SELECT b.foo FROM Bar AS b)) from Person p");
}
private JpaQueryMethod getMethod(String name, Class<?>... parameterTypes) {
try {

View File

@@ -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<User, Long> {
@Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true)

View File

@@ -718,9 +718,18 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
@Query("select u from User u")
List<UserExcerpt> 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<UserExcerpt> findRecordProjectionWithFunctions();
@Query("select u.firstname from User u")
List<String> findStringProjection();
@Query("select u.firstname, LOWER(u.lastname) from User u")
List<Object[]> findObjectArrayProjectionWithFunctions();
@Query("select u.address from User u")
List<AddressDto> findAddressProjection();
@Query("select u from User u")
<T> List<T> findRecordProjection(Class<T> projectionType);
@@ -807,6 +816,12 @@ public interface UserRepository extends JpaRepository<User, Integer>, 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) {
}