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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -885,7 +885,7 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryTokenStream> {
|
||||
builder.appendExpression(visit(ctx.variable()));
|
||||
}
|
||||
|
||||
return builder;
|
||||
return builder.toInline();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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++)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -40,8 +40,4 @@ class EclipseLinkUserRepositoryFinderTests extends UserRepositoryFinderTests {
|
||||
@Override
|
||||
void shouldProjectWithKeysetScrolling() {}
|
||||
|
||||
@Disabled
|
||||
@Override
|
||||
void rawMapProjectionWithEntityAndAggregatedValue() {}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user