Do not consider JPA-managed types projections.

We now back off from rewriting queries to constructor expressions if a returned type is a JPA-managed one.

See #3895
This commit is contained in:
Mark Paluch
2025-06-02 11:24:04 +02:00
parent c538a4fcb4
commit 64d5e70e38
2 changed files with 107 additions and 21 deletions

View File

@@ -144,13 +144,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
* @param processor
* @return
*/
private ReturnedType getReturnedType(ResultProcessor processor) {
ReturnedType getReturnedType(ResultProcessor processor) {
ReturnedType returnedType = processor.getReturnedType();
Class<?> returnedJavaType = processor.getReturnedType().getReturnedType();
if (query.isDefaultProjection() || !returnedType.isProjecting() || returnedJavaType.isInterface()
|| query.isNativeQuery()) {
if (!returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNativeQuery()) {
return returnedType;
}
@@ -160,13 +159,17 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
return returnedType;
}
if ((known != null && !known) || returnedJavaType.isArray()) {
if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)) {
if (known == null) {
knownProjections.put(returnedJavaType, false);
}
return new NonProjectingReturnedType(returnedType);
}
if (query.isDefaultProjection()) {
return returnedType;
}
String alias = query.getAlias();
String projection = query.getProjection();

View File

@@ -22,12 +22,14 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.Metamodel;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -42,6 +44,7 @@ import org.mockito.quality.Strictness;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.NativeQuery;
@@ -50,11 +53,13 @@ import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.jpa.repository.sample.UserRepository;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
@@ -84,7 +89,7 @@ class SimpleJpaQueryUnitTests {
@Mock QueryExtractor extractor;
@Mock jakarta.persistence.Query query;
@Mock TypedQuery<Long> typedQuery;
@Mock RepositoryMetadata metadata;
RepositoryMetadata metadata;
@Mock ParameterBinder binder;
@Mock Metamodel metamodel;
@@ -100,12 +105,8 @@ class SimpleJpaQueryUnitTests {
when(em.getEntityManagerFactory()).thenReturn(emf);
when(em.getDelegate()).thenReturn(em);
when(emf.createEntityManager()).thenReturn(em);
when(metadata.getRepositoryInterface()).thenReturn((Class) SampleRepository.class);
when(metadata.getDomainType()).thenReturn((Class) User.class);
when(metadata.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(User.class));
when(metadata.getReturnedDomainClass(Mockito.any(Method.class))).thenReturn((Class) User.class);
when(metadata.getReturnType(Mockito.any(Method.class)))
.thenAnswer(invocation -> TypeInformation.fromReturnTypeOf(invocation.getArgument(0)));
metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class);
Method setUp = UserRepository.class.getMethod("findByLastname", String.class);
method = new JpaQueryMethod(setUp, metadata, factory, extractor);
@@ -156,7 +157,6 @@ class SimpleJpaQueryUnitTests {
assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class);
when(em.createNativeQuery(anyString(), eq(User.class))).thenReturn(query);
when(metadata.getReturnedDomainClass(method)).thenReturn((Class) User.class);
jpaQuery.createQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { "Matthews" }));
@@ -176,7 +176,6 @@ class SimpleJpaQueryUnitTests {
assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class);
when(em.createNativeQuery(anyString(), eq(User.class))).thenReturn(query);
when(metadata.getReturnedDomainClass(method)).thenReturn((Class) User.class);
jpaQuery.createQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { "Matthews" }));
@@ -239,10 +238,11 @@ class SimpleJpaQueryUnitTests {
when(em.createNativeQuery(anyString())).thenReturn(query);
AbstractJpaQuery jpaQuery = createJpaQuery(
SampleRepository.class.getMethod("findAllWithBindingsOnlyInCountQuery", String.class, Pageable.class), Optional.empty());
SampleRepository.class.getMethod("findAllWithBindingsOnlyInCountQuery", String.class, Pageable.class),
Optional.empty());
jpaQuery.doCreateCountQuery(new JpaParametersParameterAccessor(jpaQuery.getQueryMethod().getParameters(),
new Object[]{"data", PageRequest.of(0, 10)}));
new Object[] { "data", PageRequest.of(0, 10) }));
ArgumentCaptor<String> queryStringCaptor = ArgumentCaptor.forClass(String.class);
verify(em).createQuery(queryStringCaptor.capture(), eq(Long.class));
@@ -263,6 +263,67 @@ class SimpleJpaQueryUnitTests {
verify(em, times(2)).createQuery(anyString());
}
@Test // GH-3895
void doesNotRewriteQueryReturningEntity() throws Exception {
EntityType<?> entityType = mock(EntityType.class);
when(entityType.getJavaType()).thenReturn((Class) UnrelatedType.class);
when(metamodel.getManagedTypes()).thenReturn(Set.of(entityType));
AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery(
SampleRepository.class.getMethod("selectWithJoin"));
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(
jpaQuery.getQueryMethod().getParameters(), new Object[0]);
ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor));
assertThat(queryString).startsWith("SELECT cd FROM CampaignDeal cd");
}
@Test // GH-3895
void rewriteQueryReturningDto() throws Exception {
AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery(
SampleRepository.class.getMethod("selectWithJoin"));
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(
jpaQuery.getQueryMethod().getParameters(), new Object[0]);
ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor));
assertThat(queryString).startsWith(
"SELECT new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(cd.name)");
}
@Test // GH-3895
void doesNotRewriteQueryForUnknownProperty() throws Exception {
AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery(
SampleRepository.class.getMethod("projectWithUnknownPaths"));
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(
jpaQuery.getQueryMethod().getParameters(), new Object[0]);
ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor));
assertThat(queryString).startsWith("select u.unknown from User u");
}
@Test // GH-3895
void doesNotRewriteQueryForJoinPath() throws Exception {
AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery(
SampleRepository.class.getMethod("projectWithJoinPaths"));
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(
jpaQuery.getQueryMethod().getParameters(), new Object[0]);
ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor));
assertThat(queryString).startsWith("select r.name from User u LEFT JOIN FETCH u.roles r");
}
@Test // DATAJPA-1307
void jdbcStyleParametersOnlyAllowedInNativeQueries() throws Exception {
@@ -296,7 +357,8 @@ class SimpleJpaQueryUnitTests {
return createJpaQuery(method, null);
}
private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, @Nullable String countQueryString) {
private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString,
@Nullable String countQueryString) {
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryString, countQueryString,
QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create());
@@ -305,10 +367,11 @@ class SimpleJpaQueryUnitTests {
private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional<String> countQueryString) {
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
return createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(), countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery()));
return createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(),
countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery()));
}
interface SampleRepository {
interface SampleRepository extends Repository<User, Long> {
@Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true)
List<User> findNativeByLastname(String lastname);
@@ -334,11 +397,25 @@ class SimpleJpaQueryUnitTests {
@Query("select u from User u")
Collection<UserProjection> projectWithExplicitQuery();
@Query("""
SELECT cd FROM CampaignDeal cd
LEFT JOIN FETCH cd.dealLibrary d
LEFT JOIN FETCH d.publisher p
WHERE cd.campaignId = :campaignId
""")
Collection<UnrelatedType> selectWithJoin();
@Query("select u.unknown from User u")
Collection<UnrelatedType> projectWithUnknownPaths();
@Query("select r.name from User u LEFT JOIN FETCH u.roles r")
Collection<UnrelatedType> projectWithJoinPaths();
@Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u")
List<User> findAllWithExpressionInCountQuery(Pageable pageable);
@Query(value = "select u from User u", countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}")
@Query(value = "select u from User u",
countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}")
List<User> findAllWithBindingsOnlyInCountQuery(String arg0, Pageable pageable);
// Typo in named parameter
@@ -347,4 +424,10 @@ class SimpleJpaQueryUnitTests {
}
interface UserProjection {}
static class UnrelatedType {
public UnrelatedType(String name) {}
}
}