Browse Source

Refine input property handling for DTO projections.

We now retain the input property ordering. Additionally, when providing input properties, we no longer limit properties to top-level properties but allow selection of nested properties.

Closes #4174
See: #3908
3.5.x
Mark Paluch 1 week ago
parent
commit
4d7f75e99c
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 3
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java
  2. 25
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
  3. 5
      spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java
  4. 35
      spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java

3
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java

@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.support; @@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.support;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Function;
@ -60,7 +61,7 @@ abstract class FluentQuerySupport<S, R> { @@ -60,7 +61,7 @@ abstract class FluentQuerySupport<S, R> {
this.limit = limit;
if (properties != null) {
this.properties = new HashSet<>(properties);
this.properties = new LinkedHashSet<>(properties);
} else {
this.properties = Collections.emptySet();
}

25
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

@ -788,13 +788,14 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T @@ -788,13 +788,14 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
* @param inputProperties must not be {@literal null}.
* @param scrollPosition must not be {@literal null}.
*/
private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, @Nullable Specification<S> spec,
Class<S> domainClass, Sort sort, Collection<String> inputProperties, @Nullable ScrollPosition scrollPosition) {
private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, @Nullable Specification<S> spec, Class<S> domainClass,
Sort sort, Collection<String> inputProperties, @Nullable ScrollPosition scrollPosition) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<S> query;
boolean interfaceProjection = returnedType.isInterfaceProjection();
boolean inputPropertiesPresent = !inputProperties.isEmpty();
if (returnedType.needsCustomConstruction()) {
query = (CriteriaQuery) (interfaceProjection ? builder.createTupleQuery()
@ -822,15 +823,21 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T @@ -822,15 +823,21 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
Set<String> topLevelProperties = new HashSet<>();
for (String property : requiredSelection) {
int separator = property.indexOf('.');
String topLevelProperty = separator == -1 ? property : property.substring(0, separator);
if (inputPropertiesPresent && !interfaceProjection) {
PropertyPath path = PropertyPath.from(property, returnedType.getDomainType());
selections.add(QueryUtils.toExpressionRecursively(root, path, true));
} else {
if (!topLevelProperties.add(topLevelProperty)) {
continue;
}
int separator = property.indexOf('.');
String topLevelProperty = separator == -1 ? property : property.substring(0, separator);
if (!topLevelProperties.add(topLevelProperty)) {
continue;
}
PropertyPath path = PropertyPath.from(topLevelProperty, returnedType.getDomainType());
selections.add(QueryUtils.toExpressionRecursively(root, path, true).alias(topLevelProperty));
PropertyPath path = PropertyPath.from(topLevelProperty, returnedType.getDomainType());
selections.add(QueryUtils.toExpressionRecursively(root, path, true).alias(topLevelProperty));
}
}
Class<?> typeToRead = returnedType.getReturnedType();

5
spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java

@ -39,6 +39,11 @@ import org.springframework.test.context.ContextConfiguration; @@ -39,6 +39,11 @@ import org.springframework.test.context.ContextConfiguration;
@Disabled("hsqldb seems to hang on this test class without leaving a surefire report")
class EclipseLinkNamespaceUserRepositoryTests extends NamespaceUserRepositoryTests {
@Disabled("EclipseLink does not support records, additionally, it does not support constructor creation using nested (join) properties")
@Override
@Test
public void findByFluentSpecificationWithDtoProjectionJoins() {}
/**
* Ignored until https://bugs.eclipse.org/bugs/show_bug.cgi?id=422450 is resolved.
*/

35
spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java

@ -2846,21 +2846,36 @@ class UserRepositoryTests { @@ -2846,21 +2846,36 @@ class UserRepositoryTests {
flushTestUsers();
User prototype = new User();
prototype.setFirstname("v");
record MyProjection(String name, int age, boolean active) {
record MyProjection(String name) {
}
List<MyProjection> users = repository.findBy(userHasFirstnameLike("Oliver"), //
q -> q.project("firstname", "age", "active").as(MyProjection.class).all());
assertThat(users).hasSize(1).extracting(MyProjection::name).contains(firstUser.getFirstname());
}
@Test // GH-4172
void findByFluentSpecificationWithDtoProjectionJoins() {
flushTestUsers();
firstUser.setManager(secondUser);
em.persist(firstUser);
em.flush();
record MyProjection(String name, int age, boolean active, String managerName, int managerAge) {
}
List<MyProjection> users = repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.project("firstname").as(MyProjection.class).all());
List<MyProjection> result = repository.findBy(userHasFirstnameLike("Oliver"), //
q -> q.project("firstname", "age", "active", "manager.firstname", "manager.age").as(MyProjection.class).all());
assertThat(users).extracting(MyProjection::name).containsExactlyInAnyOrder(firstUser.getFirstname(),
thirdUser.getFirstname(), fourthUser.getFirstname());
assertThat(result).hasSize(1);
MyProjection projection = result.get(0);
assertThat(projection.name()).isEqualTo(firstUser.getFirstname());
assertThat(projection.managerName()).isEqualTo(secondUser.getFirstname());
assertThat(projection.managerAge()).isEqualTo(secondUser.getAge());
}
@Test // GH-2274

Loading…
Cancel
Save