Consider project(…) properties using the fluent query API for interface projections.

We now consider input properties when selecting tuples for interface projections. DTO projections do not consider input properties as these do not necessarily match the constructor.

Closes #3716
This commit is contained in:
Mark Paluch
2025-01-15 10:43:52 +01:00
parent aa36f3c4df
commit 03d1ee9eba
6 changed files with 52 additions and 31 deletions

View File

@@ -286,7 +286,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {
AbstractJPAQuery<?, ?> query = scrollFunction.createQuery(returnedType, sort, scrollPosition);
AbstractJPAQuery<?, ?> query = scrollFunction.createQuery(FetchableFluentQueryByPredicate.this, scrollPosition);
applyQuerySettings(returnedType, limit, query, scrollPosition);

View File

@@ -23,7 +23,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -39,7 +38,6 @@ import org.springframework.data.jpa.repository.query.ScrollDelegate;
import org.springframework.data.jpa.support.PageableUtils;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.util.Assert;
@@ -57,23 +55,22 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
implements FluentQuery.FetchableFluentQuery<R> {
private final Specification<S> spec;
private final BiFunction<ReturnedType, Sort, TypedQuery<S>> finder;
private final Function<FluentQuerySupport<?, ?>, TypedQuery<S>> finder;
private final SpecificationScrollDelegate<S> scroll;
private final Function<Specification<S>, Long> countOperation;
private final Function<Specification<S>, Boolean> existsOperation;
private final EntityManager entityManager;
FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType,
BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
ProjectionFactory projectionFactory) {
Function<FluentQuerySupport<?, ?>, TypedQuery<S>> finder, SpecificationScrollDelegate<S> scrollDelegate,
Function<Specification<S>, Long> countOperation, Function<Specification<S>, Boolean> existsOperation,
EntityManager entityManager, ProjectionFactory projectionFactory) {
this(spec, entityType, (Class<R>) entityType, Sort.unsorted(), 0, Collections.emptySet(), finder, scrollDelegate,
countOperation, existsOperation, entityManager, projectionFactory);
}
private FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Class<R> resultType,
Sort sort, int limit, Collection<String> properties, BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
Sort sort, int limit, Collection<String> properties, Function<FluentQuerySupport<?, ?>, TypedQuery<S>> finder,
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
ProjectionFactory projectionFactory) {
@@ -101,8 +98,8 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
Assert.isTrue(limit >= 0, "Limit must not be negative");
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit,
properties, finder, scroll, countOperation, existsOperation, entityManager, projectionFactory);
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder,
scroll, countOperation, existsOperation, entityManager, projectionFactory);
}
@Override
@@ -155,7 +152,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
return scroll.scroll(returnedType, sort, limit, scrollPosition).map(getConversionFunction());
return scroll.scroll(this, scrollPosition).map(getConversionFunction());
}
@Override
@@ -183,7 +180,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
private TypedQuery<S> createSortedAndProjectedQuery() {
TypedQuery<S> query = finder.apply(returnedType, sort);
TypedQuery<S> query = finder.apply(this);
if (!properties.isEmpty()) {
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
@@ -235,15 +232,15 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
this.scrollFunction = scrollQueryFactory;
}
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {
public Window<T> scroll(FluentQuerySupport<?, ?> q, ScrollPosition scrollPosition) {
Query query = scrollFunction.createQuery(returnedType, sort, scrollPosition);
Query query = scrollFunction.createQuery(q, scrollPosition);
if (limit > 0) {
query = query.setMaxResults(limit);
if (q.limit > 0) {
query = query.setMaxResults(q.limit);
}
return scroll(query, sort, scrollPosition);
return scroll(query, q.sort, scrollPosition);
}
}
}

View File

@@ -95,7 +95,7 @@ abstract class FluentQuerySupport<S, R> {
}
interface ScrollQueryFactory<Q> {
Q createQuery(ReturnedType returnedType, Sort sort, ScrollPosition scrollPosition);
Q createQuery(FluentQuerySupport<?, ?> query, ScrollPosition scrollPosition);
}
}

View File

@@ -193,9 +193,10 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
return select;
};
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scroll = (returnedType, sort, scrollPosition) -> {
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scroll = (q, scrollPosition) -> {
Predicate predicateToUse = predicate;
Sort sort = q.sort;
if (scrollPosition instanceof KeysetScrollPosition keyset) {

View File

@@ -41,7 +41,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.springframework.data.domain.Example;
@@ -513,9 +512,10 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL);
Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL);
ScrollQueryFactory<TypedQuery<T>> scrollFunction = (returnedType, sort, scrollPosition) -> {
ScrollQueryFactory<TypedQuery<T>> scrollFunction = (q, scrollPosition) -> {
Specification<T> specToUse = spec;
Sort sort = q.sort;
if (scrollPosition instanceof KeysetScrollPosition keyset) {
KeysetScrollSpecification<T> keysetSpec = new KeysetScrollSpecification<>(keyset, sort, entityInformation);
@@ -523,7 +523,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
specToUse = specToUse.and(keysetSpec);
}
TypedQuery<T> query = getQuery(returnedType, specToUse, domainClass, sort, scrollPosition);
TypedQuery<T> query = getQuery(q.returnedType, specToUse, domainClass, sort, q.properties, scrollPosition);
if (scrollPosition instanceof OffsetScrollPosition offset) {
if (!offset.isInitial()) {
@@ -534,8 +534,8 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
return query;
};
BiFunction<ReturnedType, Sort, TypedQuery<T>> finder = (returnedType, sort) -> getQuery(returnedType, spec,
domainClass, sort, null);
Function<FluentQuerySupport<?, ?>, TypedQuery<T>> finder = (q) -> getQuery(q.returnedType, spec, domainClass,
q.sort, q.properties, null);
SpecificationScrollDelegate<T> scrollDelegate = new SpecificationScrollDelegate<>(scrollFunction,
entityInformation);
@@ -757,7 +757,8 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
* @param sort must not be {@literal null}.
*/
protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, Class<S> domainClass, Sort sort) {
return getQuery(ReturnedType.of(domainClass, domainClass, projectionFactory), spec, domainClass, sort, null);
return getQuery(ReturnedType.of(domainClass, domainClass, projectionFactory), spec, domainClass, sort,
Collections.emptySet(), null);
}
/**
@@ -767,17 +768,23 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
* @param spec can be {@literal null}.
* @param domainClass must not be {@literal null}.
* @param sort must not be {@literal null}.
* @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, @Nullable ScrollPosition scrollPosition) {
Class<S> domainClass, Sort sort, Collection<String> inputProperties, @Nullable ScrollPosition scrollPosition) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<S> query;
List<String> inputProperties = returnedType.getInputProperties();
boolean interfaceProjection = returnedType.getReturnedType().isInterface();
if (returnedType.needsCustomConstruction() && (inputProperties.isEmpty() || !interfaceProjection)) {
inputProperties = returnedType.getInputProperties();
}
if (returnedType.needsCustomConstruction()) {
query = (CriteriaQuery) (returnedType.getReturnedType().isInterface() ? builder.createTupleQuery()
query = (CriteriaQuery) (interfaceProjection ? builder.createTupleQuery()
: builder.createQuery(returnedType.getReturnedType()));
} else {
query = builder.createQuery(domainClass);
@@ -789,7 +796,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
Collection<String> requiredSelection;
if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) {
if (scrollPosition instanceof KeysetScrollPosition && interfaceProjection) {
requiredSelection = KeysetScrollDelegate.getProjectionInputProperties(entityInformation, inputProperties, sort);
} else {
requiredSelection = inputProperties;

View File

@@ -2697,7 +2697,7 @@ class UserRepositoryTests {
assertThat(page1.getContent()).containsExactly(fourthUser);
}
@Test // GH-2274
@Test // GH-2274, GH-3716
void findByFluentSpecificationWithInterfaceBasedProjection() {
flushTestUsers();
@@ -2707,6 +2707,14 @@ class UserRepositoryTests {
assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname)
.containsExactlyInAnyOrder(firstUser.getFirstname(), thirdUser.getFirstname(), fourthUser.getFirstname());
assertThat(users).extracting(UserProjectionInterfaceBased::getLastname).doesNotContainNull();
users = repository.findBy(userHasFirstnameLike("v"),
q -> q.as(UserProjectionInterfaceBased.class).project("firstname").all());
assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname).doesNotContainNull();
assertThat(users).extracting(UserProjectionInterfaceBased::getLastname).containsExactly(null, null, null);
}
@Test // GH-2327
@@ -2716,6 +2724,12 @@ class UserRepositoryTests {
List<UserDto> users = repository.findBy(userHasFirstnameLike("v"), q -> q.as(UserDto.class).all());
assertThat(users).extracting(UserDto::firstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
thirdUser.getFirstname(), fourthUser.getFirstname());
// project is a no-op for DTO projections as we must use the constructor as input properties
users = repository.findBy(userHasFirstnameLike("v"), q -> q.as(UserDto.class).project("lastname").all());
assertThat(users).extracting(UserDto::firstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
thirdUser.getFirstname(), fourthUser.getFirstname());
}
@@ -3467,6 +3481,8 @@ class UserRepositoryTests {
private interface UserProjectionInterfaceBased {
String getFirstname();
String getLastname();
}
record UserDto(Integer id, String firstname, String lastname, String emailAddress) {