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
This commit is contained in:
Mark Paluch
2025-05-05 17:15:27 +02:00
parent 41bfd415ca
commit 2435807db4
5 changed files with 57 additions and 17 deletions

View File

@@ -161,7 +161,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
Class<?> returnedJavaType = processor.getReturnedType().getReturnedType();
if (query.isDefaultProjection() || !returnedType.isProjecting() || returnedJavaType.isInterface()
|| query.isNativeQuery()) {
|| query.isNative()) {
return returnedType;
}
@@ -178,23 +178,23 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
return new NonProjectingReturnedType(returnedType);
}
String alias = query.getAlias();
String projection = query.getProjection();
String projectionToUse = query.<@Nullable String> doWithEnhancer(queryEnhancer -> {
// we can handle single-column and no function projections here only
if (StringUtils.hasText(projection) && (projection.indexOf(',') != -1 || projection.indexOf('(') != -1)) {
return returnedType;
}
String alias = queryEnhancer.detectAlias();
String projection = queryEnhancer.getProjection();
if (StringUtils.hasText(alias) && StringUtils.hasText(projection)) {
alias = alias.trim();
projection = projection.trim();
if (projection.startsWith(alias + ".")) {
projection = projection.substring(alias.length() + 1);
// 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(projection)) {
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(' ');
@@ -202,10 +202,15 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
projection = projection.substring(0, space);
}
return projection;
});
if (StringUtils.hasText(projectionToUse)) {
Class<?> propertyType;
try {
PropertyPath from = PropertyPath.from(projection, getQueryMethod().getEntityInformation().getJavaType());
PropertyPath from = PropertyPath.from(projectionToUse, getQueryMethod().getEntityInformation().getJavaType());
propertyType = from.getLeafType();
} catch (PropertyReferenceException ignored) {
propertyType = null;
@@ -223,7 +228,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
return returnedType;
}
String getSortedQueryString(Sort sort, ReturnedType returnedType) {
QueryProvider getSortedQuery(Sort sort, ReturnedType returnedType) {
return querySortRewriter.getSorted(query, sort, returnedType);
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.jpa.repository.query;
import java.util.List;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
@@ -46,6 +47,11 @@ class DefaultEntityQuery implements EntityQuery, DeclaredQuery {
this.queryEnhancer = queryEnhancerFactory.create(query);
}
@Override
public <T> T doWithEnhancer(Function<QueryEnhancer, T> function) {
return function.apply(queryEnhancer);
}
@Override
public boolean isNative() {
return query.isNative();

View File

@@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.query;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
@@ -33,6 +34,8 @@ enum EmptyIntrospectedQuery implements EntityQuery {
EmptyIntrospectedQuery() {}
@Override
public boolean hasParameterBindings() {
return false;
@@ -57,11 +60,21 @@ enum EmptyIntrospectedQuery implements EntityQuery {
return null;
}
@Override
public <T> T doWithEnhancer(Function<QueryEnhancer, T> function) {
return null;
}
@Override
public boolean hasConstructorExpression() {
return false;
}
@Override
public boolean isNative() {
return false;
}
@Override
public boolean isDefaultProjection() {
return false;

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
/**
@@ -45,6 +47,15 @@ public interface EntityQuery extends ParametrizedQuery {
return new DefaultEntityQuery(preparsed, enhancerFactory);
}
/**
* Apply a {@link Function} to the query enhancer used by this query.
*
* @param function the callback function.
* @return
* @param <T>
*/
<T extends @Nullable Object> T doWithEnhancer(Function<QueryEnhancer, T> function);
/**
* Returns whether the query is using a constructor expression.
*
@@ -52,6 +63,11 @@ public interface EntityQuery extends ParametrizedQuery {
*/
boolean hasConstructorExpression();
/**
* @return whether the underlying query has at least one named parameter.
*/
boolean isNative();
/**
* Returns whether the query uses the default projection, i.e. returns the main alias defined for the query.
*/

View File

@@ -30,7 +30,7 @@ import java.util.List;
* @see EntityQuery#create(DeclaredQuery, QueryEnhancerSelector)
* @see TemplatedQuery#create(String, JpaQueryMethod, JpaQueryConfiguration)
*/
interface ParametrizedQuery extends QueryProvider {
public interface ParametrizedQuery extends QueryProvider {
/**
* @return whether the underlying query has at least one parameter.