Skip to content

Commit fb876e9

Browse files
committed
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.
1 parent c9d1161 commit fb876e9

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
@@ -101,10 +101,13 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
101101

102102
this.parser = parser;
103103
this.queryRewriter = queryRewriter;
104+
ReturnedType returnedType = method.getResultProcessor().getReturnedType();
104105

105106
JpaParameters parameters = method.getParameters();
106-
if (parameters.hasPageableParameter() || parameters.hasSortParameter()) {
107+
if ((parameters.hasPageableParameter() || parameters.hasSortParameter()) && !parameters.hasDynamicProjection()) {
107108
this.querySortRewriter = new CachingQuerySortRewriter();
109+
} else if (returnedType.isProjecting() && !returnedType.getReturnedType().isInterface()) {
110+
this.querySortRewriter = new ProjectingSortRewriter();
108111
} else {
109112
this.querySortRewriter = NoOpQuerySortRewriter.INSTANCE;
110113
}
@@ -117,9 +120,8 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
117120
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
118121

119122
Sort sort = accessor.getSort();
120-
String sortedQueryString = getSortedQueryString(sort);
121-
122123
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
124+
String sortedQueryString = getSortedQueryString(sort, processor.getReturnedType());
123125

124126
Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), processor.getReturnedType());
125127

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

133-
String getSortedQueryString(Sort sort) {
134-
return querySortRewriter.getSorted(query, sort);
135+
String getSortedQueryString(Sort sort, ReturnedType returnedType) {
136+
return querySortRewriter.getSorted(query, sort, returnedType);
135137
}
136138

137139
@Override
@@ -213,24 +215,25 @@ protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nulla
213215

214216
String applySorting(CachableQuery cachableQuery) {
215217

216-
return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).applySorting(cachableQuery.getSort(),
217-
cachableQuery.getAlias());
218+
return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).rewrite(cachableQuery.getSort(),
219+
cachableQuery.getReturnedType());
218220
}
219221

220222
/**
221223
* Query Sort Rewriter interface.
222224
*/
223225
interface QuerySortRewriter {
224-
String getSorted(DeclaredQuery query, Sort sort);
226+
String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType);
225227
}
226228

227229
/**
228230
* No-op query rewriter.
229231
*/
230232
enum NoOpQuerySortRewriter implements QuerySortRewriter {
233+
231234
INSTANCE;
232235

233-
public String getSorted(DeclaredQuery query, Sort sort) {
236+
public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
234237

235238
if (sort.isSorted()) {
236239
throw new UnsupportedOperationException("NoOpQueryCache does not support sorting");
@@ -240,6 +243,25 @@ public String getSorted(DeclaredQuery query, Sort sort) {
240243
}
241244
}
242245

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

273+
private volatile String cachedQueryString;
274+
251275
@Override
252-
public String getSorted(DeclaredQuery query, Sort sort) {
276+
public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
253277

254278
if (sort.isUnsorted()) {
255-
return query.getQueryString();
279+
280+
String cachedQueryString = this.cachedQueryString;
281+
if (cachedQueryString == null) {
282+
this.cachedQueryString = cachedQueryString = queryCache.get(new CachableQuery(query, sort, returnedType));
283+
}
284+
285+
return cachedQueryString;
256286
}
257287

258-
return queryCache.get(new CachableQuery(query, sort));
288+
return queryCache.get(new CachableQuery(query, sort, returnedType));
259289
}
260290
}
261291

@@ -271,12 +301,14 @@ static class CachableQuery {
271301
private final DeclaredQuery declaredQuery;
272302
private final String queryString;
273303
private final Sort sort;
304+
private final ReturnedType returnedType;
274305

275-
CachableQuery(DeclaredQuery query, Sort sort) {
306+
CachableQuery(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
276307

277308
this.declaredQuery = query;
278309
this.queryString = query.getQueryString();
279310
this.sort = sort;
311+
this.returnedType = returnedType;
280312
}
281313

282314
DeclaredQuery getDeclaredQuery() {
@@ -287,9 +319,8 @@ Sort getSort() {
287319
return sort;
288320
}
289321

290-
@Nullable
291-
String getAlias() {
292-
return declaredQuery.getAlias();
322+
public ReturnedType getReturnedType() {
323+
return returnedType;
293324
}
294325

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