Skip to content

Commit 64d5e70

Browse files
committed
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
1 parent c538a4f commit 64d5e70

File tree

2 files changed

+107
-21
lines changed

2 files changed

+107
-21
lines changed

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,12 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
144144
* @param processor
145145
* @return
146146
*/
147-
private ReturnedType getReturnedType(ResultProcessor processor) {
147+
ReturnedType getReturnedType(ResultProcessor processor) {
148148

149149
ReturnedType returnedType = processor.getReturnedType();
150150
Class<?> returnedJavaType = processor.getReturnedType().getReturnedType();
151151

152-
if (query.isDefaultProjection() || !returnedType.isProjecting() || returnedJavaType.isInterface()
153-
|| query.isNativeQuery()) {
152+
if (!returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNativeQuery()) {
154153
return returnedType;
155154
}
156155

@@ -160,13 +159,17 @@ private ReturnedType getReturnedType(ResultProcessor processor) {
160159
return returnedType;
161160
}
162161

163-
if ((known != null && !known) || returnedJavaType.isArray()) {
162+
if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)) {
164163
if (known == null) {
165164
knownProjections.put(returnedJavaType, false);
166165
}
167166
return new NonProjectingReturnedType(returnedType);
168167
}
169168

169+
if (query.isDefaultProjection()) {
170+
return returnedType;
171+
}
172+
170173
String alias = query.getAlias();
171174
String projection = query.getProjection();
172175

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
import jakarta.persistence.EntityManagerFactory;
2323
import jakarta.persistence.Tuple;
2424
import jakarta.persistence.TypedQuery;
25+
import jakarta.persistence.metamodel.EntityType;
2526
import jakarta.persistence.metamodel.Metamodel;
2627

2728
import java.lang.reflect.Method;
2829
import java.util.Collection;
2930
import java.util.List;
3031
import java.util.Optional;
32+
import java.util.Set;
3133

3234
import org.junit.jupiter.api.BeforeEach;
3335
import org.junit.jupiter.api.Test;
@@ -42,6 +44,7 @@
4244
import org.springframework.data.domain.Page;
4345
import org.springframework.data.domain.PageRequest;
4446
import org.springframework.data.domain.Pageable;
47+
import org.springframework.data.domain.Sort;
4548
import org.springframework.data.jpa.domain.sample.User;
4649
import org.springframework.data.jpa.provider.QueryExtractor;
4750
import org.springframework.data.jpa.repository.NativeQuery;
@@ -50,11 +53,13 @@
5053
import org.springframework.data.jpa.repository.sample.UserRepository;
5154
import org.springframework.data.projection.ProjectionFactory;
5255
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
56+
import org.springframework.data.repository.Repository;
5357
import org.springframework.data.repository.core.RepositoryMetadata;
58+
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
5459
import org.springframework.data.repository.query.Param;
5560
import org.springframework.data.repository.query.RepositoryQuery;
61+
import org.springframework.data.repository.query.ResultProcessor;
5662
import org.springframework.data.repository.query.ValueExpressionDelegate;
57-
import org.springframework.data.util.TypeInformation;
5863
import org.springframework.lang.Nullable;
5964

6065
/**
@@ -84,7 +89,7 @@ class SimpleJpaQueryUnitTests {
8489
@Mock QueryExtractor extractor;
8590
@Mock jakarta.persistence.Query query;
8691
@Mock TypedQuery<Long> typedQuery;
87-
@Mock RepositoryMetadata metadata;
92+
RepositoryMetadata metadata;
8893
@Mock ParameterBinder binder;
8994
@Mock Metamodel metamodel;
9095

@@ -100,12 +105,8 @@ void setUp() throws SecurityException, NoSuchMethodException {
100105
when(em.getEntityManagerFactory()).thenReturn(emf);
101106
when(em.getDelegate()).thenReturn(em);
102107
when(emf.createEntityManager()).thenReturn(em);
103-
when(metadata.getRepositoryInterface()).thenReturn((Class) SampleRepository.class);
104-
when(metadata.getDomainType()).thenReturn((Class) User.class);
105-
when(metadata.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(User.class));
106-
when(metadata.getReturnedDomainClass(Mockito.any(Method.class))).thenReturn((Class) User.class);
107-
when(metadata.getReturnType(Mockito.any(Method.class)))
108-
.thenAnswer(invocation -> TypeInformation.fromReturnTypeOf(invocation.getArgument(0)));
108+
109+
metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class);
109110

110111
Method setUp = UserRepository.class.getMethod("findByLastname", String.class);
111112
method = new JpaQueryMethod(setUp, metadata, factory, extractor);
@@ -156,7 +157,6 @@ void discoversNativeQuery() throws Exception {
156157
assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class);
157158

158159
when(em.createNativeQuery(anyString(), eq(User.class))).thenReturn(query);
159-
when(metadata.getReturnedDomainClass(method)).thenReturn((Class) User.class);
160160

161161
jpaQuery.createQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { "Matthews" }));
162162

@@ -176,7 +176,6 @@ void discoversNativeQueryFromNativeQueryInterface() throws Exception {
176176
assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class);
177177

178178
when(em.createNativeQuery(anyString(), eq(User.class))).thenReturn(query);
179-
when(metadata.getReturnedDomainClass(method)).thenReturn((Class) User.class);
180179

181180
jpaQuery.createQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { "Matthews" }));
182181

@@ -239,10 +238,11 @@ void allowsCountQueryUsingParametersNotInOriginalQuery() throws Exception {
239238
when(em.createNativeQuery(anyString())).thenReturn(query);
240239

241240
AbstractJpaQuery jpaQuery = createJpaQuery(
242-
SampleRepository.class.getMethod("findAllWithBindingsOnlyInCountQuery", String.class, Pageable.class), Optional.empty());
241+
SampleRepository.class.getMethod("findAllWithBindingsOnlyInCountQuery", String.class, Pageable.class),
242+
Optional.empty());
243243

244244
jpaQuery.doCreateCountQuery(new JpaParametersParameterAccessor(jpaQuery.getQueryMethod().getParameters(),
245-
new Object[]{"data", PageRequest.of(0, 10)}));
245+
new Object[] { "data", PageRequest.of(0, 10) }));
246246

247247
ArgumentCaptor<String> queryStringCaptor = ArgumentCaptor.forClass(String.class);
248248
verify(em).createQuery(queryStringCaptor.capture(), eq(Long.class));
@@ -263,6 +263,67 @@ void projectsWithManuallyDeclaredQuery() throws Exception {
263263
verify(em, times(2)).createQuery(anyString());
264264
}
265265

266+
@Test // GH-3895
267+
void doesNotRewriteQueryReturningEntity() throws Exception {
268+
269+
EntityType<?> entityType = mock(EntityType.class);
270+
when(entityType.getJavaType()).thenReturn((Class) UnrelatedType.class);
271+
when(metamodel.getManagedTypes()).thenReturn(Set.of(entityType));
272+
273+
AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery(
274+
SampleRepository.class.getMethod("selectWithJoin"));
275+
276+
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(
277+
jpaQuery.getQueryMethod().getParameters(), new Object[0]);
278+
ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
279+
String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor));
280+
281+
assertThat(queryString).startsWith("SELECT cd FROM CampaignDeal cd");
282+
}
283+
284+
@Test // GH-3895
285+
void rewriteQueryReturningDto() throws Exception {
286+
287+
AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery(
288+
SampleRepository.class.getMethod("selectWithJoin"));
289+
290+
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(
291+
jpaQuery.getQueryMethod().getParameters(), new Object[0]);
292+
ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
293+
String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor));
294+
295+
assertThat(queryString).startsWith(
296+
"SELECT new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(cd.name)");
297+
}
298+
299+
@Test // GH-3895
300+
void doesNotRewriteQueryForUnknownProperty() throws Exception {
301+
302+
AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery(
303+
SampleRepository.class.getMethod("projectWithUnknownPaths"));
304+
305+
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(
306+
jpaQuery.getQueryMethod().getParameters(), new Object[0]);
307+
ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
308+
String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor));
309+
310+
assertThat(queryString).startsWith("select u.unknown from User u");
311+
}
312+
313+
@Test // GH-3895
314+
void doesNotRewriteQueryForJoinPath() throws Exception {
315+
316+
AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery(
317+
SampleRepository.class.getMethod("projectWithJoinPaths"));
318+
319+
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(
320+
jpaQuery.getQueryMethod().getParameters(), new Object[0]);
321+
ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
322+
String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor));
323+
324+
assertThat(queryString).startsWith("select r.name from User u LEFT JOIN FETCH u.roles r");
325+
}
326+
266327
@Test // DATAJPA-1307
267328
void jdbcStyleParametersOnlyAllowedInNativeQueries() throws Exception {
268329

@@ -296,7 +357,8 @@ private AbstractJpaQuery createJpaQuery(Method method) {
296357
return createJpaQuery(method, null);
297358
}
298359

299-
private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, @Nullable String countQueryString) {
360+
private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString,
361+
@Nullable String countQueryString) {
300362

301363
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryString, countQueryString,
302364
QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create());
@@ -305,10 +367,11 @@ private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable St
305367
private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional<String> countQueryString) {
306368

307369
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
308-
return createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(), countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery()));
370+
return createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(),
371+
countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery()));
309372
}
310373

311-
interface SampleRepository {
374+
interface SampleRepository extends Repository<User, Long> {
312375

313376
@Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true)
314377
List<User> findNativeByLastname(String lastname);
@@ -334,11 +397,25 @@ interface SampleRepository {
334397
@Query("select u from User u")
335398
Collection<UserProjection> projectWithExplicitQuery();
336399

400+
@Query("""
401+
SELECT cd FROM CampaignDeal cd
402+
LEFT JOIN FETCH cd.dealLibrary d
403+
LEFT JOIN FETCH d.publisher p
404+
WHERE cd.campaignId = :campaignId
405+
""")
406+
Collection<UnrelatedType> selectWithJoin();
407+
408+
@Query("select u.unknown from User u")
409+
Collection<UnrelatedType> projectWithUnknownPaths();
410+
411+
@Query("select r.name from User u LEFT JOIN FETCH u.roles r")
412+
Collection<UnrelatedType> projectWithJoinPaths();
413+
337414
@Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u")
338415
List<User> findAllWithExpressionInCountQuery(Pageable pageable);
339416

340-
341-
@Query(value = "select u from User u", countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}")
417+
@Query(value = "select u from User u",
418+
countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}")
342419
List<User> findAllWithBindingsOnlyInCountQuery(String arg0, Pageable pageable);
343420

344421
// Typo in named parameter
@@ -347,4 +424,10 @@ interface SampleRepository {
347424
}
348425

349426
interface UserProjection {}
427+
428+
static class UnrelatedType {
429+
430+
public UnrelatedType(String name) {}
431+
432+
}
350433
}

0 commit comments

Comments
 (0)