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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user