Skip to content

Commit 2435807

Browse files
committed
Avoid DTO Constructor Expression rewriting for selection of nested properties.
We back off from rewriting String-based queries to use DTO Constructor expressions if the query selects a property that is assignable to the return type. Closes #3862
1 parent 41bfd41 commit 2435807

File tree

5 files changed

+57
-17
lines changed

5 files changed

+57
-17
lines changed

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

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ private ReturnedType getReturnedType(ResultProcessor processor) {
161161
Class<?> returnedJavaType = processor.getReturnedType().getReturnedType();
162162

163163
if (query.isDefaultProjection() || !returnedType.isProjecting() || returnedJavaType.isInterface()
164-
|| query.isNativeQuery()) {
164+
|| query.isNative()) {
165165
return returnedType;
166166
}
167167

@@ -178,34 +178,39 @@ private ReturnedType getReturnedType(ResultProcessor processor) {
178178
return new NonProjectingReturnedType(returnedType);
179179
}
180180

181-
String alias = query.getAlias();
182-
String projection = query.getProjection();
181+
String projectionToUse = query.<@Nullable String> doWithEnhancer(queryEnhancer -> {
183182

184-
// we can handle single-column and no function projections here only
185-
if (StringUtils.hasText(projection) && (projection.indexOf(',') != -1 || projection.indexOf('(') != -1)) {
186-
return returnedType;
187-
}
183+
String alias = queryEnhancer.detectAlias();
184+
String projection = queryEnhancer.getProjection();
188185

189-
if (StringUtils.hasText(alias) && StringUtils.hasText(projection)) {
190-
alias = alias.trim();
191-
projection = projection.trim();
192-
if (projection.startsWith(alias + ".")) {
193-
projection = projection.substring(alias.length() + 1);
186+
// we can handle single-column and no function projections here only
187+
if (StringUtils.hasText(projection) && (projection.indexOf(',') != -1 || projection.indexOf('(') != -1)) {
188+
return null;
194189
}
195-
}
196190

197-
if (StringUtils.hasText(projection)) {
191+
if (StringUtils.hasText(alias) && StringUtils.hasText(projection)) {
192+
alias = alias.trim();
193+
projection = projection.trim();
194+
if (projection.startsWith(alias + ".")) {
195+
projection = projection.substring(alias.length() + 1);
196+
}
197+
}
198198

199199
int space = projection.indexOf(' ');
200200

201201
if (space != -1) {
202202
projection = projection.substring(0, space);
203203
}
204204

205+
return projection;
206+
});
207+
208+
if (StringUtils.hasText(projectionToUse)) {
209+
205210
Class<?> propertyType;
206211

207212
try {
208-
PropertyPath from = PropertyPath.from(projection, getQueryMethod().getEntityInformation().getJavaType());
213+
PropertyPath from = PropertyPath.from(projectionToUse, getQueryMethod().getEntityInformation().getJavaType());
209214
propertyType = from.getLeafType();
210215
} catch (PropertyReferenceException ignored) {
211216
propertyType = null;
@@ -223,7 +228,7 @@ private ReturnedType getReturnedType(ResultProcessor processor) {
223228
return returnedType;
224229
}
225230

226-
String getSortedQueryString(Sort sort, ReturnedType returnedType) {
231+
QueryProvider getSortedQuery(Sort sort, ReturnedType returnedType) {
227232
return querySortRewriter.getSorted(query, sort, returnedType);
228233
}
229234

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.jpa.repository.query;
1717

1818
import java.util.List;
19+
import java.util.function.Function;
1920

2021
import org.jspecify.annotations.Nullable;
2122

@@ -46,6 +47,11 @@ class DefaultEntityQuery implements EntityQuery, DeclaredQuery {
4647
this.queryEnhancer = queryEnhancerFactory.create(query);
4748
}
4849

50+
@Override
51+
public <T> T doWithEnhancer(Function<QueryEnhancer, T> function) {
52+
return function.apply(queryEnhancer);
53+
}
54+
4955
@Override
5056
public boolean isNative() {
5157
return query.isNative();

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Collections;
1919
import java.util.List;
20+
import java.util.function.Function;
2021

2122
import org.jspecify.annotations.Nullable;
2223

@@ -33,6 +34,8 @@ enum EmptyIntrospectedQuery implements EntityQuery {
3334

3435
EmptyIntrospectedQuery() {}
3536

37+
38+
3639
@Override
3740
public boolean hasParameterBindings() {
3841
return false;
@@ -57,11 +60,21 @@ public List<ParameterBinding> getParameterBindings() {
5760
return null;
5861
}
5962

63+
@Override
64+
public <T> T doWithEnhancer(Function<QueryEnhancer, T> function) {
65+
return null;
66+
}
67+
6068
@Override
6169
public boolean hasConstructorExpression() {
6270
return false;
6371
}
6472

73+
@Override
74+
public boolean isNative() {
75+
return false;
76+
}
77+
6578
@Override
6679
public boolean isDefaultProjection() {
6780
return false;

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18+
import java.util.function.Function;
19+
1820
import org.jspecify.annotations.Nullable;
1921

2022
/**
@@ -45,13 +47,27 @@ static EntityQuery create(DeclaredQuery query, QueryEnhancerSelector selector) {
4547
return new DefaultEntityQuery(preparsed, enhancerFactory);
4648
}
4749

50+
/**
51+
* Apply a {@link Function} to the query enhancer used by this query.
52+
*
53+
* @param function the callback function.
54+
* @return
55+
* @param <T>
56+
*/
57+
<T extends @Nullable Object> T doWithEnhancer(Function<QueryEnhancer, T> function);
58+
4859
/**
4960
* Returns whether the query is using a constructor expression.
5061
*
5162
* @since 1.10
5263
*/
5364
boolean hasConstructorExpression();
5465

66+
/**
67+
* @return whether the underlying query has at least one named parameter.
68+
*/
69+
boolean isNative();
70+
5571
/**
5672
* Returns whether the query uses the default projection, i.e. returns the main alias defined for the query.
5773
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* @see EntityQuery#create(DeclaredQuery, QueryEnhancerSelector)
3131
* @see TemplatedQuery#create(String, JpaQueryMethod, JpaQueryConfiguration)
3232
*/
33-
interface ParametrizedQuery extends QueryProvider {
33+
public interface ParametrizedQuery extends QueryProvider {
3434

3535
/**
3636
* @return whether the underlying query has at least one parameter.

0 commit comments

Comments
 (0)