Add tests for dto, iterface and raw projections.
There's a difference in what the query needs to look like using dto vs. interface projections where the former does not allow column aliases and the latter requires them. See: #2327 Original Pull Request: #3654
This commit is contained in:
@@ -23,6 +23,8 @@ import jakarta.persistence.Tuple;
|
||||
import jakarta.persistence.TupleElement;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
@@ -51,6 +53,7 @@ import org.springframework.data.util.Lazy;
|
||||
import org.springframework.jdbc.support.JdbcUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Abstract base class to implement {@link RepositoryQuery}s.
|
||||
@@ -353,6 +356,24 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
|
||||
}
|
||||
}
|
||||
|
||||
if(type.isProjecting() && !type.getReturnedType().isInterface() && !type.getInputProperties().isEmpty()) {
|
||||
List<Object> ctorArgs = new ArrayList<>(type.getInputProperties().size());
|
||||
type.getInputProperties().forEach(it -> {
|
||||
ctorArgs.add(tuple.get(it));
|
||||
});
|
||||
try {
|
||||
return type.getReturnedType().getConstructor(ctorArgs.stream().map(Object::getClass).toArray(Class<?>[]::new)).newInstance(ctorArgs.toArray());
|
||||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return new TupleBackedMap(tupleWrapper.apply(tuple));
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,13 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
|
||||
Sort sort = accessor.getSort();
|
||||
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
|
||||
String sortedQueryString = getSortedQueryString(sort, processor.getReturnedType());
|
||||
|
||||
String sortedQueryString = null;
|
||||
if(querySortRewriter.equals(NoOpQuerySortRewriter.INSTANCE) && accessor.findDynamicProjection() != null && !accessor.findDynamicProjection().isInterface()) {
|
||||
sortedQueryString = getSortedQueryString(new ProjectingSortRewriter(), query, sort, processor.getReturnedType());
|
||||
} else {
|
||||
sortedQueryString = getSortedQueryString(sort, processor.getReturnedType());
|
||||
}
|
||||
|
||||
Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), processor.getReturnedType());
|
||||
|
||||
@@ -134,6 +140,10 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
return querySortRewriter.getSorted(query, sort, returnedType);
|
||||
}
|
||||
|
||||
private static String getSortedQueryString(QuerySortRewriter rewriter, DeclaredQuery query, Sort sort, ReturnedType returnedType) {
|
||||
return rewriter.getSorted(query, sort, returnedType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ParameterBinder createBinder() {
|
||||
return createBinder(query);
|
||||
|
||||
@@ -36,4 +36,8 @@ class EclipseLinkUserRepositoryFinderTests extends UserRepositoryFinderTests {
|
||||
@Override
|
||||
void executesInKeywordForPageCorrectly() {}
|
||||
|
||||
@Disabled
|
||||
@Override
|
||||
void rawMapProjectionWithEntityAndAggregatedValue() {}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,18 +15,24 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.domain.Sort.Direction.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.springframework.data.domain.Sort.Direction.ASC;
|
||||
import static org.springframework.data.domain.Sort.Direction.DESC;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.data.Offset;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.domain.Limit;
|
||||
@@ -45,6 +51,9 @@ import org.springframework.data.jpa.repository.sample.UserRepository;
|
||||
import org.springframework.data.jpa.repository.sample.UserRepository.IdOnly;
|
||||
import org.springframework.data.jpa.repository.sample.UserRepository.NameOnly;
|
||||
import org.springframework.data.jpa.repository.sample.UserRepository.RolesAndFirstname;
|
||||
import org.springframework.data.jpa.repository.sample.UserRepository.UserExcerpt;
|
||||
import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountDtoProjection;
|
||||
import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountInterfaceProjection;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
@@ -247,9 +256,9 @@ class UserRepositoryFinderTests {
|
||||
@Test // GH-3409
|
||||
void executesWindowQueryWithPageable() {
|
||||
|
||||
Window<User> first = userRepository.findByLastnameOrderByFirstname("Matthews", PageRequest.of(0,1));
|
||||
Window<User> first = userRepository.findByLastnameOrderByFirstname("Matthews", PageRequest.of(0, 1));
|
||||
|
||||
Window<User> next = userRepository.findByLastnameOrderByFirstname("Matthews", PageRequest.of(1,1));
|
||||
Window<User> next = userRepository.findByLastnameOrderByFirstname("Matthews", PageRequest.of(1, 1));
|
||||
|
||||
assertThat(first).containsExactly(dave);
|
||||
assertThat(next).containsExactly(oliver);
|
||||
@@ -406,21 +415,92 @@ class UserRepositoryFinderTests {
|
||||
assertThat(result).containsExactly(carter);
|
||||
}
|
||||
|
||||
@Test // GH-3076
|
||||
void dtoProjectionShouldApplyConstructorExpressionRewriting() {
|
||||
@Test // GH-3076
|
||||
void dtoProjectionShouldApplyConstructorExpressionRewriting() {
|
||||
|
||||
List<UserRepository.UserExcerpt> dtos = userRepository.findRecordProjection();
|
||||
List<UserExcerpt> dtos = userRepository.findRecordProjection();
|
||||
|
||||
assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) //
|
||||
.contains("Dave", "Carter", "Oliver August");
|
||||
}
|
||||
assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) //
|
||||
.contains("Dave", "Carter", "Oliver August");
|
||||
}
|
||||
|
||||
@Test // GH-3076
|
||||
void dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() {
|
||||
@Test // GH-3076
|
||||
void dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() {
|
||||
|
||||
List<UserRepository.UserExcerpt> dtos = userRepository.findMultiselectRecordProjection();
|
||||
List<UserExcerpt> dtos = userRepository.findMultiselectRecordProjection();
|
||||
|
||||
assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) //
|
||||
.contains("Dave", "Carter", "Oliver August");
|
||||
}
|
||||
|
||||
@Test // GH-3076
|
||||
void dynamicDtoProjection() {
|
||||
|
||||
List<UserExcerpt> dtos = userRepository.findRecordProjection(UserExcerpt.class);
|
||||
|
||||
assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) //
|
||||
.contains("Dave", "Carter", "Oliver August");
|
||||
}
|
||||
|
||||
@Test // GH-3076
|
||||
void dtoProjectionWithEntityAndAggregatedValue() {
|
||||
|
||||
Map<String, User> musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave,
|
||||
oliver.getFirstname(), oliver);
|
||||
|
||||
assertThat(userRepository.dtoProjectionEntityAndAggregatedValue()).allSatisfy(projection -> {
|
||||
assertThat(projection.user()).isIn(musicians.values());
|
||||
assertThat(projection.roleCount()).isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(),
|
||||
Offset.offset(0L));
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-3076
|
||||
void interfaceProjectionWithEntityAndAggregatedValue() {
|
||||
|
||||
Map<String, User> musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave,
|
||||
oliver.getFirstname(), oliver);
|
||||
|
||||
assertThat(userRepository.interfaceProjectionEntityAndAggregatedValue()).allSatisfy(projection -> {
|
||||
assertThat(projection.getUser()).isIn(musicians.values());
|
||||
assertThat(projection.getRoleCount())
|
||||
.isCloseTo(musicians.get(projection.getUser().getFirstname()).getRoles().size(), Offset.offset(0L));
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-3076
|
||||
void rawMapProjectionWithEntityAndAggregatedValue() {
|
||||
|
||||
Map<String, User> musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave,
|
||||
oliver.getFirstname(), oliver);
|
||||
|
||||
assertThat(userRepository.rawMapProjectionEntityAndAggregatedValue()).allSatisfy(projection -> {
|
||||
assertThat(projection.get("user")).isIn(musicians.values());
|
||||
assertThat(projection).containsKey("roleCount");
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-3076
|
||||
void dtoProjectionWithEntityAndAggregatedValueWithPageable() {
|
||||
|
||||
Map<String, User> musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave,
|
||||
oliver.getFirstname(), oliver);
|
||||
|
||||
assertThat(
|
||||
userRepository.dtoProjectionEntityAndAggregatedValue(PageRequest.of(0, 10).withSort(Sort.by("firstname"))))
|
||||
.allSatisfy(projection -> {
|
||||
assertThat(projection.user()).isIn(musicians.values());
|
||||
assertThat(projection.roleCount())
|
||||
.isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(), Offset.offset(0L));
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedTest // GH-3076
|
||||
@ValueSource(classes = { UserRoleCountDtoProjection.class, UserRoleCountInterfaceProjection.class })
|
||||
<T> void dynamicProjectionWithEntityAndAggregated(Class<T> resultType) {
|
||||
|
||||
assertThat(userRepository.findMultiselectRecordDynamicProjection(resultType)).hasSize(3)
|
||||
.hasOnlyElementsOfType(resultType);
|
||||
}
|
||||
|
||||
assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) //
|
||||
.contains("Dave", "Carter", "Oliver August");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.data.jpa.repository.sample;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.QueryHint;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -724,11 +726,33 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
|
||||
@Query("select u from User u")
|
||||
List<UserExcerpt> findRecordProjection();
|
||||
|
||||
@Query("select u from User u")
|
||||
<T> List<T> findRecordProjection(Class<T> projectionType);
|
||||
|
||||
@Query("select u.firstname, u.lastname from User u")
|
||||
List<UserExcerpt> findMultiselectRecordProjection();
|
||||
|
||||
@UserRoleCountProjectingQuery
|
||||
List<UserRoleCountDtoProjection> dtoProjectionEntityAndAggregatedValue();
|
||||
|
||||
@UserRoleCountProjectingQuery
|
||||
Page<UserRoleCountDtoProjection> dtoProjectionEntityAndAggregatedValue(PageRequest page);
|
||||
|
||||
@Query("select u as user, count(r) as roleCount from User u left outer join u.roles r group by u")
|
||||
List<UserRoleCountInterfaceProjection> interfaceProjectionEntityAndAggregatedValue();
|
||||
|
||||
@Query("select u as user, count(r) as roleCount from User u left outer join u.roles r group by u")
|
||||
List<Map<String, Object>> rawMapProjectionEntityAndAggregatedValue();
|
||||
|
||||
@UserRoleCountProjectingQuery
|
||||
<T> List<T> findMultiselectRecordDynamicProjection(Class<T> projectionType);
|
||||
|
||||
Window<User> findBy(OffsetScrollPosition position);
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Query("select u, count(r) from User u left outer join u.roles r group by u")
|
||||
@interface UserRoleCountProjectingQuery {}
|
||||
|
||||
interface RolesAndFirstname {
|
||||
|
||||
String getFirstname();
|
||||
@@ -757,4 +781,11 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
|
||||
|
||||
}
|
||||
|
||||
record UserRoleCountDtoProjection(User user, Long roleCount) {}
|
||||
|
||||
interface UserRoleCountInterfaceProjection {
|
||||
User getUser();
|
||||
Long getRoleCount();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -66,6 +66,13 @@ This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM U
|
||||
This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM USER u`.
|
||||
====
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
JPQL constructor expressions must not contain aliases for selected columns.
|
||||
While `SELECT u as user, count(u.roles) as roleCount FROM USER u ...` is a valid usecase for interface based projections that rely on column names from the returned `Tuple`, the same construct is invalid when requesting a DTO where it needs to be `SELECT u, count(u.roles) FROM USER u ...`. +
|
||||
Some persistence providers may be lenient about this, others not.
|
||||
====
|
||||
|
||||
Repository query methods that return a DTO projection type (a Java type outside the domain type hierarchy) are subject for query rewriting.
|
||||
If an `@Query`-annotated query already uses constructor expressions, then Spring Data backs off and doesn't apply DTO constructor expression rewriting.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user