Skip to content

Commit e0348c8

Browse files
mp911dechristophstrobl
authored andcommitted
Rewrite string-queries to use constructor expressions when return type is DTO.
We now rewrite String-based JPA queries to use constructor expressions when either selecting the entity or selecting individual properties. We do not rewrite queries that already use constructor expressions. Closes #3076
1 parent cbe7876 commit e0348c8

19 files changed

+615
-37
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ protected Class<?> getTypeToRead(ReturnedType returnedType) {
283283
return null;
284284
}
285285

286-
return returnedType.isProjecting() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) //
286+
return returnedType.isProjecting() && returnedType.getReturnedType().isInterface()
287+
&& !getMetamodel().isJpaManaged(returnedType.getReturnedType()) //
287288
? Tuple.class //
288289
: null;
289290
}

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

+47-16
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,13 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
9999
});
100100

101101
this.queryRewriter = queryRewriter;
102+
ReturnedType returnedType = method.getResultProcessor().getReturnedType();
102103

103104
JpaParameters parameters = method.getParameters();
104-
if (parameters.hasPageableParameter() || parameters.hasSortParameter()) {
105+
if ((parameters.hasPageableParameter() || parameters.hasSortParameter()) && !parameters.hasDynamicProjection()) {
105106
this.querySortRewriter = new CachingQuerySortRewriter();
107+
} else if (returnedType.isProjecting() && !returnedType.getReturnedType().isInterface()) {
108+
this.querySortRewriter = new ProjectingSortRewriter();
106109
} else {
107110
this.querySortRewriter = NoOpQuerySortRewriter.INSTANCE;
108111
}
@@ -115,9 +118,8 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
115118
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
116119

117120
Sort sort = accessor.getSort();
118-
String sortedQueryString = getSortedQueryString(sort);
119-
120121
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
122+
String sortedQueryString = getSortedQueryString(sort, processor.getReturnedType());
121123

122124
Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), processor.getReturnedType());
123125

@@ -128,8 +130,8 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
128130
return parameterBinder.get().bindAndPrepare(query, metadata, accessor);
129131
}
130132

131-
String getSortedQueryString(Sort sort) {
132-
return querySortRewriter.getSorted(query, sort);
133+
String getSortedQueryString(Sort sort, ReturnedType returnedType) {
134+
return querySortRewriter.getSorted(query, sort, returnedType);
133135
}
134136

135137
@Override
@@ -211,24 +213,25 @@ protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nulla
211213

212214
String applySorting(CachableQuery cachableQuery) {
213215

214-
return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).applySorting(cachableQuery.getSort(),
215-
cachableQuery.getAlias());
216+
return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).rewrite(cachableQuery.getSort(),
217+
cachableQuery.getReturnedType());
216218
}
217219

218220
/**
219221
* Query Sort Rewriter interface.
220222
*/
221223
interface QuerySortRewriter {
222-
String getSorted(DeclaredQuery query, Sort sort);
224+
String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType);
223225
}
224226

225227
/**
226228
* No-op query rewriter.
227229
*/
228230
enum NoOpQuerySortRewriter implements QuerySortRewriter {
231+
229232
INSTANCE;
230233

231-
public String getSorted(DeclaredQuery query, Sort sort) {
234+
public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
232235

233236
if (sort.isSorted()) {
234237
throw new UnsupportedOperationException("NoOpQueryCache does not support sorting");
@@ -238,6 +241,25 @@ public String getSorted(DeclaredQuery query, Sort sort) {
238241
}
239242
}
240243

244+
static class ProjectingSortRewriter implements QuerySortRewriter {
245+
246+
private volatile String cachedQueryString;
247+
248+
public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
249+
250+
if (sort.isSorted()) {
251+
throw new UnsupportedOperationException("NoOpQueryCache does not support sorting");
252+
}
253+
254+
String cachedQueryString = this.cachedQueryString;
255+
if (cachedQueryString == null) {
256+
this.cachedQueryString = cachedQueryString = QueryEnhancerFactory.forQuery(query).rewrite(sort, returnedType);
257+
}
258+
259+
return cachedQueryString;
260+
}
261+
}
262+
241263
/**
242264
* Caching variant of {@link QuerySortRewriter}.
243265
*/
@@ -246,14 +268,22 @@ class CachingQuerySortRewriter implements QuerySortRewriter {
246268
private final ConcurrentLruCache<CachableQuery, String> queryCache = new ConcurrentLruCache<>(16,
247269
AbstractStringBasedJpaQuery.this::applySorting);
248270

271+
private volatile String cachedQueryString;
272+
249273
@Override
250-
public String getSorted(DeclaredQuery query, Sort sort) {
274+
public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
251275

252276
if (sort.isUnsorted()) {
253-
return query.getQueryString();
277+
278+
String cachedQueryString = this.cachedQueryString;
279+
if (cachedQueryString == null) {
280+
this.cachedQueryString = cachedQueryString = queryCache.get(new CachableQuery(query, sort, returnedType));
281+
}
282+
283+
return cachedQueryString;
254284
}
255285

256-
return queryCache.get(new CachableQuery(query, sort));
286+
return queryCache.get(new CachableQuery(query, sort, returnedType));
257287
}
258288
}
259289

@@ -269,12 +299,14 @@ static class CachableQuery {
269299
private final DeclaredQuery declaredQuery;
270300
private final String queryString;
271301
private final Sort sort;
302+
private final ReturnedType returnedType;
272303

273-
CachableQuery(DeclaredQuery query, Sort sort) {
304+
CachableQuery(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
274305

275306
this.declaredQuery = query;
276307
this.queryString = query.getQueryString();
277308
this.sort = sort;
309+
this.returnedType = returnedType;
278310
}
279311

280312
DeclaredQuery getDeclaredQuery() {
@@ -285,9 +317,8 @@ Sort getSort() {
285317
return sort;
286318
}
287319

288-
@Nullable
289-
String getAlias() {
290-
return declaredQuery.getAlias();
320+
public ReturnedType getReturnedType() {
321+
return returnedType;
291322
}
292323

293324
@Override

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

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Set;
1919

2020
import org.springframework.data.domain.Sort;
21+
import org.springframework.data.repository.query.ReturnedType;
2122
import org.springframework.lang.Nullable;
2223

2324
/**
@@ -52,6 +53,11 @@ public String applySorting(Sort sort, @Nullable String alias) {
5253
return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
5354
}
5455

56+
@Override
57+
public String rewrite(Sort sort, ReturnedType returnedType) {
58+
return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
59+
}
60+
5561
@Override
5662
public String createCountQueryFor(@Nullable String countProjection) {
5763
return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, this.query.isNativeQuery());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.query;
17+
18+
import static org.springframework.data.jpa.repository.query.QueryTokens.*;
19+
20+
import org.springframework.data.repository.query.ReturnedType;
21+
22+
/**
23+
* HQL Query Transformer that rewrites the query using constructor expressions.
24+
* <p>
25+
* Query rewriting from a plain property/object selection towards constructor expression only works if either:
26+
* <ul>
27+
* <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>
29+
* </ul>
30+
*
31+
* @author Mark Paluch
32+
*/
33+
class DtoProjectionTransformerDelegate {
34+
35+
private final ReturnedType returnedType;
36+
37+
public DtoProjectionTransformerDelegate(ReturnedType returnedType) {
38+
this.returnedType = returnedType;
39+
}
40+
41+
public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) {
42+
43+
if (!returnedType.isProjecting() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
44+
return selectionList;
45+
}
46+
47+
QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder();
48+
builder.append(QueryTokens.TOKEN_NEW);
49+
builder.append(QueryTokens.token(returnedType.getReturnedType().getName()));
50+
builder.append(QueryTokens.TOKEN_OPEN_PAREN);
51+
52+
// assume the selection points to the document
53+
if (selectionList.size() == 1) {
54+
55+
builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> {
56+
57+
QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder();
58+
prop.append(QueryTokens.token(selectionList.getFirst().value()));
59+
prop.append(QueryTokens.TOKEN_DOT);
60+
prop.append(QueryTokens.token(property));
61+
62+
return prop.build();
63+
}, QueryTokens.TOKEN_COMMA));
64+
65+
} else {
66+
builder.appendInline(selectionList);
67+
}
68+
69+
builder.append(QueryTokens.TOKEN_CLOSE_PAREN);
70+
71+
return builder.build();
72+
}
73+
}

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

+24-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.data.domain.Sort;
2323
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
24+
import org.springframework.data.repository.query.ReturnedType;
2425
import org.springframework.lang.Nullable;
2526
import org.springframework.util.Assert;
2627
import org.springframework.util.ObjectUtils;
@@ -40,13 +41,15 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer {
4041
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
4142
private final Sort sort;
4243
private final @Nullable String primaryFromAlias;
44+
private final @Nullable DtoProjectionTransformerDelegate dtoDelegate;
4345

44-
EqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
46+
EqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) {
4547

4648
Assert.notNull(sort, "Sort must not be null");
4749

4850
this.sort = sort;
4951
this.primaryFromAlias = primaryFromAlias;
52+
this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType);
5053
}
5154

5255
@Override
@@ -80,6 +83,26 @@ public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementCont
8083
return builder;
8184
}
8285

86+
@Override
87+
public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) {
88+
89+
if (dtoDelegate == null) {
90+
return super.visitSelect_clause(ctx);
91+
}
92+
93+
QueryRendererBuilder builder = QueryRenderer.builder();
94+
95+
builder.append(QueryTokens.expression(ctx.SELECT()));
96+
97+
if (ctx.DISTINCT() != null) {
98+
builder.append(QueryTokens.expression(ctx.DISTINCT()));
99+
}
100+
101+
QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA);
102+
103+
return builder.append(dtoDelegate.transformSelectionList(tokenStream));
104+
}
105+
83106
private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx, Sort sort) {
84107

85108
if (ctx.orderby_clause() != null) {

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

+24
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.data.domain.Sort;
2323
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
24+
import org.springframework.data.repository.query.ReturnedType;
2425
import org.springframework.lang.Nullable;
2526
import org.springframework.util.Assert;
2627
import org.springframework.util.ObjectUtils;
@@ -38,13 +39,24 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
3839
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
3940
private final Sort sort;
4041
private final @Nullable String primaryFromAlias;
42+
private final @Nullable DtoProjectionTransformerDelegate dtoDelegate;
4143

4244
HqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
4345

4446
Assert.notNull(sort, "Sort must not be null");
4547

4648
this.sort = sort;
4749
this.primaryFromAlias = primaryFromAlias;
50+
this.dtoDelegate = null;
51+
}
52+
53+
HqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) {
54+
55+
Assert.notNull(sort, "Sort must not be null");
56+
57+
this.sort = sort;
58+
this.primaryFromAlias = primaryFromAlias;
59+
this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType);
4860
}
4961

5062
@Override
@@ -81,6 +93,18 @@ public QueryRendererBuilder visitOrderedQuery(HqlParser.OrderedQueryContext ctx)
8193
return visitOrderedQuery(ctx, this.sort);
8294
}
8395

96+
@Override
97+
public QueryTokenStream visitSelectionList(HqlParser.SelectionListContext ctx) {
98+
99+
QueryTokenStream tokenStream = super.visitSelectionList(ctx);
100+
101+
if (dtoDelegate != null && !isSubquery(ctx)) {
102+
return dtoDelegate.transformSelectionList(tokenStream);
103+
}
104+
105+
return tokenStream;
106+
}
107+
84108
@Override
85109
public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) {
86110

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

+6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.util.StringJoiner;
5151

5252
import org.springframework.data.domain.Sort;
53+
import org.springframework.data.repository.query.ReturnedType;
5354
import org.springframework.lang.Nullable;
5455
import org.springframework.util.Assert;
5556
import org.springframework.util.CollectionUtils;
@@ -300,6 +301,11 @@ public String applySorting(Sort sort) {
300301
return applySorting(sort, detectAlias());
301302
}
302303

304+
@Override
305+
public String rewrite(Sort sort, ReturnedType returnedType) {
306+
return applySorting(sort, primaryAlias);
307+
}
308+
303309
@Override
304310
public String applySorting(Sort sort, @Nullable String alias) {
305311

0 commit comments

Comments
 (0)