diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java index 6631a3fec3..75207eb8ac 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java @@ -286,7 +286,7 @@ class PredicateScrollDelegate extends ScrollDelegate { public Window 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); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java index 87ade63ff3..dab32519d1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java @@ -23,7 +23,6 @@ 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.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 extends FluentQuerySupport implements FluentQuery.FetchableFluentQuery { private final Specification spec; - private final BiFunction> finder; + private final Function, TypedQuery> finder; private final SpecificationScrollDelegate scroll; private final Function, Long> countOperation; private final Function, Boolean> existsOperation; private final EntityManager entityManager; FetchableFluentQueryBySpecification(Specification spec, Class entityType, - BiFunction> finder, - SpecificationScrollDelegate scrollDelegate, Function, Long> countOperation, - Function, Boolean> existsOperation, EntityManager entityManager, - ProjectionFactory projectionFactory) { + Function, TypedQuery> finder, SpecificationScrollDelegate scrollDelegate, + Function, Long> countOperation, Function, Boolean> existsOperation, + EntityManager entityManager, ProjectionFactory projectionFactory) { this(spec, entityType, (Class) entityType, Sort.unsorted(), 0, Collections.emptySet(), finder, scrollDelegate, countOperation, existsOperation, entityManager, projectionFactory); } private FetchableFluentQueryBySpecification(Specification spec, Class entityType, Class resultType, - Sort sort, int limit, Collection properties, BiFunction> finder, + Sort sort, int limit, Collection properties, Function, TypedQuery> finder, SpecificationScrollDelegate scrollDelegate, Function, Long> countOperation, Function, Boolean> existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) { @@ -101,8 +98,8 @@ public FetchableFluentQuery limit(int limit) { 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 @@ public Window scroll(ScrollPosition scrollPosition) { 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 @@ public boolean exists() { private TypedQuery createSortedAndProjectedQuery() { - TypedQuery query = finder.apply(returnedType, sort); + TypedQuery query = finder.apply(this); if (!properties.isEmpty()) { query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties)); @@ -235,15 +232,15 @@ static class SpecificationScrollDelegate extends ScrollDelegate { this.scrollFunction = scrollQueryFactory; } - public Window scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) { + public Window 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); } } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java index b48f49eef8..f97da41ee1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java @@ -95,7 +95,7 @@ final Function getConversionFunction(Class inputType, Class tar } interface ScrollQueryFactory { - Q createQuery(ReturnedType returnedType, Sort sort, ScrollPosition scrollPosition); + Q createQuery(FluentQuerySupport query, ScrollPosition scrollPosition); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index 7c6889e5dc..0008e92ff3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java @@ -193,9 +193,10 @@ public R findBy(Predicate predicate, Function> scroll = (returnedType, sort, scrollPosition) -> { + ScrollQueryFactory> scroll = (q, scrollPosition) -> { Predicate predicateToUse = predicate; + Sort sort = q.sort; if (scrollPosition instanceof KeysetScrollPosition keyset) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 8345ecf510..45a5c5fa79 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -41,7 +41,6 @@ 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 @@ private R doFindBy(Specification spec, Class domainClass, Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL); Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL); - ScrollQueryFactory> scrollFunction = (returnedType, sort, scrollPosition) -> { + ScrollQueryFactory> scrollFunction = (q, scrollPosition) -> { Specification specToUse = spec; + Sort sort = q.sort; if (scrollPosition instanceof KeysetScrollPosition keyset) { KeysetScrollSpecification keysetSpec = new KeysetScrollSpecification<>(keyset, sort, entityInformation); @@ -523,7 +523,7 @@ private R doFindBy(Specification spec, Class domainClass, specToUse = specToUse.and(keysetSpec); } - TypedQuery query = getQuery(returnedType, specToUse, domainClass, sort, scrollPosition); + TypedQuery query = getQuery(q.returnedType, specToUse, domainClass, sort, q.properties, scrollPosition); if (scrollPosition instanceof OffsetScrollPosition offset) { if (!offset.isInitial()) { @@ -534,8 +534,8 @@ private R doFindBy(Specification spec, Class domainClass, return query; }; - BiFunction> finder = (returnedType, sort) -> getQuery(returnedType, spec, - domainClass, sort, null); + Function, TypedQuery> finder = (q) -> getQuery(q.returnedType, spec, domainClass, + q.sort, q.properties, null); SpecificationScrollDelegate scrollDelegate = new SpecificationScrollDelegate<>(scrollFunction, entityInformation); @@ -757,7 +757,8 @@ protected TypedQuery getQuery(@Nullable Specification spec, Sort sort) { * @param sort must not be {@literal null}. */ protected TypedQuery getQuery(@Nullable Specification spec, Class 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 @@ protected TypedQuery getQuery(@Nullable Specification spec, * @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 TypedQuery getQuery(ReturnedType returnedType, @Nullable Specification spec, - Class domainClass, Sort sort, @Nullable ScrollPosition scrollPosition) { + Class domainClass, Sort sort, Collection inputProperties, @Nullable ScrollPosition scrollPosition) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query; - List 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 @@ private TypedQuery getQuery(ReturnedType returnedType, @Nullabl Collection requiredSelection; - if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) { + if (scrollPosition instanceof KeysetScrollPosition && interfaceProjection) { requiredSelection = KeysetScrollDelegate.getProjectionInputProperties(entityInformation, inputProperties, sort); } else { requiredSelection = inputProperties; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 48e963ed7e..a34d7c640d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -2697,7 +2697,7 @@ void findByFluentSpecificationPage() { assertThat(page1.getContent()).containsExactly(fourthUser); } - @Test // GH-2274 + @Test // GH-2274, GH-3716 void findByFluentSpecificationWithInterfaceBasedProjection() { flushTestUsers(); @@ -2707,6 +2707,14 @@ void findByFluentSpecificationWithInterfaceBasedProjection() { 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 @@ void findByFluentSpecificationWithDtoProjection() { List 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 @@ private Page executeSpecWithSort(Sort sort) { private interface UserProjectionInterfaceBased { String getFirstname(); + + String getLastname(); } record UserDto(Integer id, String firstname, String lastname, String emailAddress) {