From 2435807db4c00179a847bf2f98e4ee81697ab14d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 May 2025 17:15:27 +0200 Subject: [PATCH] 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 --- .../query/AbstractStringBasedJpaQuery.java | 37 +++++++++++-------- .../repository/query/DefaultEntityQuery.java | 6 +++ .../query/EmptyIntrospectedQuery.java | 13 +++++++ .../jpa/repository/query/EntityQuery.java | 16 ++++++++ .../repository/query/ParametrizedQuery.java | 2 +- 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index 61d5ea7f3..64b21e8cb 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -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); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java index bde36d153..d07e238f2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java @@ -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 doWithEnhancer(Function function) { + return function.apply(queryEnhancer); + } + @Override public boolean isNative() { return query.isNative(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java index a0ef2363b..188b0b8c2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java @@ -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 doWithEnhancer(Function function) { + return null; + } + @Override public boolean hasConstructorExpression() { return false; } + @Override + public boolean isNative() { + return false; + } + @Override public boolean isDefaultProjection() { return false; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java index f827e0b29..0e22efa28 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java @@ -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 doWithEnhancer(Function 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. */ diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java index 85a314127..4736e091f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java @@ -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.