Skip to content

Commit

Permalink
Polishing.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
christophstrobl authored and mp911de committed Dec 3, 2024
1 parent 1a532b8 commit 0483e44
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
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;
Expand Down Expand Up @@ -51,6 +53,7 @@
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.
Expand Down Expand Up @@ -353,6 +356,24 @@ public Object convert(Object source) {
}
}

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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,13 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) {

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());

Expand All @@ -134,6 +140,10 @@ String getSortedQueryString(Sort sort, ReturnedType returnedType) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ void executesNotInQueryCorrectly() {}
@Override
void executesInKeywordForPageCorrectly() {}

@Disabled
@Override
void rawMapProjectionWithEntityAndAggregatedValue() {}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -45,6 +51,9 @@
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;
Expand Down Expand Up @@ -247,9 +256,9 @@ void executesQueryWithLimitAndScrollPosition() {
@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);
Expand Down Expand Up @@ -406,21 +415,92 @@ void findByNegatingSimplePropertyUsingMixedNullNonNullArgument() {
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() {

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 dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() {
@Test // GH-3076
void interfaceProjectionWithEntityAndAggregatedValue() {

List<UserRepository.UserExcerpt> dtos = userRepository.findMultiselectRecordProjection();
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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ void shouldTranslateSingleProjectionToDto() {
"SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p");
}

// @Test // GH-3076
// void xxx() {
//
// JpaQueryMethod method = getMethod("dtoProjection2");
// JpqlSortedQueryTransformer transformer = new JpqlSortedQueryTransformer(Sort.unsorted(), null,
// method.getResultProcessor().getReturnedType());
//
// JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery("select u.foo, u.bar, count(r) from User u left outer join u.role r group by u");
//
// QueryTokenStream visit = transformer.visit(parser.getContext());
//
// assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
// "select new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord2(u.foo, u.bar, count(r)) from User u left outer join u.role r group by u");
// }

@Test // GH-3076
void shouldRewriteQueriesWithSubselect() {

Expand Down Expand Up @@ -100,6 +115,7 @@ private JpaQueryMethod getMethod(String name, Class<?>... parameterTypes) {
interface MyRepo extends Repository<Person, String> {

MyRecord dtoProjection();
MyRecord2 dtoProjection2();
}

record Person(String id) {
Expand All @@ -109,4 +125,8 @@ record Person(String id) {
record MyRecord(String foo, String bar) {

}

record MyRecord2(String foo, String bar, Integer count) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
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;
Expand Down Expand Up @@ -721,11 +723,33 @@ List<String> findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter
@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();
Expand Down Expand Up @@ -754,4 +778,11 @@ record UserExcerpt(String firstname, String lastname) {

}

record UserRoleCountDtoProjection(User user, Long roleCount) {}

interface UserRoleCountInterfaceProjection {
User getUser();
Long getRoleCount();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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.

Expand Down

0 comments on commit 0483e44

Please sign in to comment.