Skip to content

Commit 5a22fbf

Browse files
committed
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
1 parent 61a1a8c commit 5a22fbf

17 files changed

+592
-339
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import jakarta.persistence.TypedQuery;
2525

2626
import java.lang.reflect.Constructor;
27+
import java.util.AbstractMap;
2728
import java.util.ArrayList;
2829
import java.util.Arrays;
2930
import java.util.Collection;
@@ -474,7 +475,7 @@ private static boolean areAssignmentCompatible(Class<?> to, Class<?> from) {
474475
*
475476
* @author Jens Schauder
476477
*/
477-
private static class TupleBackedMap implements Map<String, Object> {
478+
private static class TupleBackedMap extends AbstractMap<String, Object> implements Map<String, Object> {
478479

479480
private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified";
480481

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
import org.springframework.data.domain.Sort;
2828
import org.springframework.data.expression.ValueEvaluationContextProvider;
2929
import org.springframework.data.jpa.repository.QueryRewriter;
30-
import org.springframework.data.mapping.PropertyPath;
31-
import org.springframework.data.mapping.PropertyReferenceException;
3230
import org.springframework.data.repository.query.ResultProcessor;
3331
import org.springframework.data.repository.query.ReturnedType;
3432
import org.springframework.data.repository.query.ValueExpressionDelegate;
@@ -166,52 +164,7 @@ ReturnedType getReturnedType(ResultProcessor processor) {
166164
return new NonProjectingReturnedType(returnedType);
167165
}
168166

169-
if (query.isDefaultProjection()) {
170-
return returnedType;
171-
}
172-
173-
String alias = query.getAlias();
174-
String projection = query.getProjection();
175-
176-
// we can handle single-column and no function projections here only
177-
if (StringUtils.hasText(projection) && (projection.indexOf(',') != -1 || projection.indexOf('(') != -1)) {
178-
return returnedType;
179-
}
180-
181-
if (StringUtils.hasText(alias) && StringUtils.hasText(projection)) {
182-
alias = alias.trim();
183-
projection = projection.trim();
184-
if (projection.startsWith(alias + ".")) {
185-
projection = projection.substring(alias.length() + 1);
186-
}
187-
}
188-
189-
if (StringUtils.hasText(projection)) {
190-
191-
int space = projection.indexOf(' ');
192-
193-
if (space != -1) {
194-
projection = projection.substring(0, space);
195-
}
196-
197-
Class<?> propertyType;
198-
199-
try {
200-
PropertyPath from = PropertyPath.from(projection, getQueryMethod().getEntityInformation().getJavaType());
201-
propertyType = from.getLeafType();
202-
} catch (PropertyReferenceException ignored) {
203-
propertyType = null;
204-
}
205-
206-
if (propertyType == null
207-
|| (returnedJavaType.isAssignableFrom(propertyType) || propertyType.isAssignableFrom(returnedJavaType))) {
208-
knownProjections.put(returnedJavaType, false);
209-
return new NonProjectingReturnedType(returnedType);
210-
} else {
211-
knownProjections.put(returnedJavaType, true);
212-
}
213-
}
214-
167+
knownProjections.put(returnedJavaType, true);
215168
return returnedType;
216169
}
217170

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717

1818
import static org.springframework.data.jpa.repository.query.QueryTokens.*;
1919

20+
import java.util.ArrayList;
21+
import java.util.Iterator;
22+
import java.util.List;
23+
import java.util.function.Function;
24+
2025
import org.springframework.data.repository.query.ReturnedType;
2126

2227
/**
@@ -25,7 +30,8 @@
2530
* Query rewriting from a plain property/object selection towards constructor expression only works if either:
2631
* <ul>
2732
* <li>The query selects its primary alias ({@code SELECT p FROM Person p})</li>
28-
* <li>The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p})</li>
33+
* <li>The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p},
34+
* {@code SELECT COUNT(p.foo), p.bar AS bar FROM Person p})</li>
2935
* </ul>
3036
*
3137
* @author Mark Paluch
@@ -34,42 +40,94 @@
3440
class DtoProjectionTransformerDelegate {
3541

3642
private final ReturnedType returnedType;
43+
private final boolean applyRewriting;
44+
private final List<QueryTokenStream> selectItems = new ArrayList<>();
3745

3846
public DtoProjectionTransformerDelegate(ReturnedType returnedType) {
3947
this.returnedType = returnedType;
48+
this.applyRewriting = returnedType.isProjecting() && !returnedType.getReturnedType().isInterface()
49+
&& returnedType.needsCustomConstruction();
50+
}
51+
52+
public boolean applyRewriting() {
53+
return applyRewriting;
54+
}
55+
56+
public boolean canRewrite() {
57+
return applyRewriting() && !selectItems.isEmpty();
58+
}
59+
60+
public void appendSelectItem(QueryTokenStream selectItem) {
61+
62+
if (applyRewriting()) {
63+
selectItems.add(new DetachedStream(selectItem));
64+
}
4065
}
4166

42-
public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) {
67+
public QueryTokenStream getRewrittenSelectionList() {
68+
69+
if (canRewrite()) {
70+
71+
QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder();
72+
builder.append(QueryTokens.TOKEN_NEW);
73+
builder.append(QueryTokens.token(returnedType.getReturnedType().getName()));
74+
builder.append(QueryTokens.TOKEN_OPEN_PAREN);
75+
76+
if (selectItems.size() == 1 && selectItems.get(0).size() == 1) {
4377

44-
if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface()
45-
|| !returnedType.needsCustomConstruction() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
46-
return selectionList;
78+
builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> {
79+
80+
QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder();
81+
prop.appendInline(selectItems.get(0));
82+
prop.append(QueryTokens.TOKEN_DOT);
83+
prop.append(QueryTokens.token(property));
84+
85+
return prop.build();
86+
}, QueryTokens.TOKEN_COMMA));
87+
} else {
88+
builder.append(QueryTokenStream.concat(selectItems, Function.identity(), TOKEN_COMMA));
89+
}
90+
91+
builder.append(TOKEN_CLOSE_PAREN);
92+
93+
return builder.build();
4794
}
4895

49-
QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder();
50-
builder.append(QueryTokens.TOKEN_NEW);
51-
builder.append(QueryTokens.token(returnedType.getReturnedType().getName()));
52-
builder.append(QueryTokens.TOKEN_OPEN_PAREN);
96+
return QueryTokenStream.empty();
97+
}
98+
99+
private static class DetachedStream extends QueryRenderer {
53100

54-
// assume the selection points to the document
55-
if (selectionList.size() == 1) {
101+
private final QueryTokenStream delegate;
56102

57-
builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> {
103+
private DetachedStream(QueryTokenStream delegate) {
104+
this.delegate = delegate;
105+
}
58106

59-
QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder();
60-
prop.append(QueryTokens.token(selectionList.getFirst().value()));
61-
prop.append(QueryTokens.TOKEN_DOT);
62-
prop.append(QueryTokens.token(property));
107+
@Override
108+
public boolean isExpression() {
109+
return delegate.isExpression();
110+
}
63111

64-
return prop.build();
65-
}, QueryTokens.TOKEN_COMMA));
112+
@Override
113+
public int size() {
114+
return delegate.size();
115+
}
66116

67-
} else {
68-
builder.appendInline(selectionList);
117+
@Override
118+
public boolean isEmpty() {
119+
return delegate.isEmpty();
69120
}
70121

71-
builder.append(QueryTokens.TOKEN_CLOSE_PAREN);
122+
@Override
123+
public Iterator<QueryToken> iterator() {
124+
return delegate.iterator();
125+
}
72126

73-
return builder.build();
127+
@Override
128+
public String render() {
129+
return delegate instanceof QueryRenderer ? ((QueryRenderer) delegate).render() : delegate.toString();
130+
}
74131
}
132+
75133
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,7 @@ public QueryTokenStream visitJoin_condition(EqlParser.Join_conditionContext ctx)
292292
}
293293

294294
@Override
295-
public QueryTokenStream visitJoin_association_path_expression(
296-
EqlParser.Join_association_path_expressionContext ctx) {
295+
public QueryTokenStream visitJoin_association_path_expression(EqlParser.Join_association_path_expressionContext ctx) {
297296

298297
QueryRendererBuilder builder = QueryRenderer.builder();
299298

@@ -451,8 +450,7 @@ public QueryTokenStream visitSingle_valued_path_expression(EqlParser.Single_valu
451450
}
452451

453452
@Override
454-
public QueryTokenStream visitGeneral_identification_variable(
455-
EqlParser.General_identification_variableContext ctx) {
453+
public QueryTokenStream visitGeneral_identification_variable(EqlParser.General_identification_variableContext ctx) {
456454

457455
QueryRendererBuilder builder = QueryRenderer.builder();
458456

@@ -641,6 +639,15 @@ public QueryTokenStream visitDelete_clause(EqlParser.Delete_clauseContext ctx) {
641639
@Override
642640
public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) {
643641

642+
QueryRendererBuilder builder = prepareSelectClause(ctx);
643+
644+
builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA));
645+
646+
return builder;
647+
}
648+
649+
QueryRendererBuilder prepareSelectClause(EqlParser.Select_clauseContext ctx) {
650+
644651
QueryRendererBuilder builder = QueryRenderer.builder();
645652

646653
builder.append(QueryTokens.expression(ctx.SELECT()));
@@ -649,8 +656,6 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) {
649656
builder.append(QueryTokens.expression(ctx.DISTINCT()));
650657
}
651658

652-
builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA));
653-
654659
return builder;
655660
}
656661

@@ -2448,8 +2453,7 @@ public QueryTokenStream visitFunction_name(EqlParser.Function_nameContext ctx) {
24482453
}
24492454

24502455
@Override
2451-
public QueryTokenStream visitCharacter_valued_input_parameter(
2452-
EqlParser.Character_valued_input_parameterContext ctx) {
2456+
public QueryTokenStream visitCharacter_valued_input_parameter(EqlParser.Character_valued_input_parameterContext ctx) {
24532457

24542458
if (ctx.CHARACTER() != null) {
24552459
return QueryRendererBuilder.from(QueryTokens.expression(ctx.CHARACTER()));

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,53 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) {
9191
return super.visitSelect_clause(ctx);
9292
}
9393

94-
QueryRendererBuilder builder = QueryRenderer.builder();
94+
QueryRendererBuilder builder = prepareSelectClause(ctx);
9595

96-
builder.append(QueryTokens.expression(ctx.SELECT()));
96+
QueryTokenStream selectItems = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA);
9797

98-
if (ctx.DISTINCT() != null) {
99-
builder.append(QueryTokens.expression(ctx.DISTINCT()));
98+
if (dtoDelegate != null && dtoDelegate.canRewrite()) {
99+
builder.append(dtoDelegate.getRewrittenSelectionList());
100+
} else {
101+
builder.append(selectItems);
100102
}
101103

102-
QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA);
104+
return builder;
105+
}
106+
107+
@Override
108+
public QueryTokenStream visitSelect_item(EqlParser.Select_itemContext ctx) {
109+
110+
QueryTokenStream tokens = super.visitSelect_item(ctx);
111+
112+
if (ctx.result_variable() != null && !tokens.isEmpty()) {
113+
transformerSupport.registerAlias(ctx.result_variable().getText());
114+
}
115+
116+
return tokens;
117+
}
118+
119+
@Override
120+
public QueryTokenStream visitSelect_expression(EqlParser.Select_expressionContext ctx) {
121+
122+
QueryTokenStream selectItem = super.visitSelect_expression(ctx);
123+
124+
if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.constructor_expression() == null) {
125+
dtoDelegate.appendSelectItem(selectItem);
126+
}
127+
128+
return selectItem;
129+
}
130+
131+
@Override
132+
public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) {
133+
134+
QueryTokenStream tokens = super.visitJoin(ctx);
135+
136+
if (ctx.identification_variable() != null) {
137+
transformerSupport.registerAlias(ctx.identification_variable().getText());
138+
}
103139

104-
return builder.append(dtoDelegate.transformSelectionList(tokenStream));
140+
return tokens;
105141
}
106142

107143
private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx, Sort sort) {
@@ -131,28 +167,4 @@ private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_state
131167
}
132168
}
133169

134-
@Override
135-
public QueryTokenStream visitSelect_item(EqlParser.Select_itemContext ctx) {
136-
137-
QueryTokenStream tokens = super.visitSelect_item(ctx);
138-
139-
if (ctx.result_variable() != null && !tokens.isEmpty()) {
140-
transformerSupport.registerAlias(tokens.getLast());
141-
}
142-
143-
return tokens;
144-
}
145-
146-
@Override
147-
public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) {
148-
149-
QueryTokenStream tokens = super.visitJoin(ctx);
150-
151-
if (!tokens.isEmpty()) {
152-
transformerSupport.registerAlias(tokens.getLast());
153-
}
154-
155-
return tokens;
156-
}
157-
158170
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,12 @@ public QueryRendererBuilder visitFromQuery(HqlParser.FromQueryContext ctx) {
103103

104104
if (ctx.fromClause() != null) {
105105
builder.appendExpression(visit(ctx.fromClause()));
106-
if(primaryFromAlias == null) {
106+
if (primaryFromAlias == null) {
107107
builder.append(TOKEN_AS);
108108
builder.append(TOKEN_DOUBLE_UNDERSCORE);
109109
}
110110
}
111111

112-
113112
if (ctx.whereClause() != null) {
114113
builder.appendExpression(visit(ctx.whereClause()));
115114
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ public QueryTokenStream visitSelection(HqlParser.SelectionContext ctx) {
830830
builder.appendExpression(visit(ctx.variable()));
831831
}
832832

833-
return builder;
833+
return builder.toInline();
834834
}
835835

836836
@Override

0 commit comments

Comments
 (0)