Skip to content

Commit 03d1ee9

Browse files
committed
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
1 parent aa36f3c commit 03d1ee9

File tree

6 files changed

+52
-31
lines changed

6 files changed

+52
-31
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ class PredicateScrollDelegate<T> extends ScrollDelegate<T> {
286286

287287
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {
288288

289-
AbstractJPAQuery<?, ?> query = scrollFunction.createQuery(returnedType, sort, scrollPosition);
289+
AbstractJPAQuery<?, ?> query = scrollFunction.createQuery(FetchableFluentQueryByPredicate.this, scrollPosition);
290290

291291
applyQuerySettings(returnedType, limit, query, scrollPosition);
292292

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

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.util.Collection;
2424
import java.util.Collections;
2525
import java.util.List;
26-
import java.util.function.BiFunction;
2726
import java.util.function.Function;
2827
import java.util.stream.Stream;
2928

@@ -39,7 +38,6 @@
3938
import org.springframework.data.jpa.support.PageableUtils;
4039
import org.springframework.data.projection.ProjectionFactory;
4140
import org.springframework.data.repository.query.FluentQuery;
42-
import org.springframework.data.repository.query.ReturnedType;
4341
import org.springframework.data.support.PageableExecutionUtils;
4442
import org.springframework.util.Assert;
4543

@@ -57,23 +55,22 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
5755
implements FluentQuery.FetchableFluentQuery<R> {
5856

5957
private final Specification<S> spec;
60-
private final BiFunction<ReturnedType, Sort, TypedQuery<S>> finder;
58+
private final Function<FluentQuerySupport<?, ?>, TypedQuery<S>> finder;
6159
private final SpecificationScrollDelegate<S> scroll;
6260
private final Function<Specification<S>, Long> countOperation;
6361
private final Function<Specification<S>, Boolean> existsOperation;
6462
private final EntityManager entityManager;
6563

6664
FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType,
67-
BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
68-
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
69-
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
70-
ProjectionFactory projectionFactory) {
65+
Function<FluentQuerySupport<?, ?>, TypedQuery<S>> finder, SpecificationScrollDelegate<S> scrollDelegate,
66+
Function<Specification<S>, Long> countOperation, Function<Specification<S>, Boolean> existsOperation,
67+
EntityManager entityManager, ProjectionFactory projectionFactory) {
7168
this(spec, entityType, (Class<R>) entityType, Sort.unsorted(), 0, Collections.emptySet(), finder, scrollDelegate,
7269
countOperation, existsOperation, entityManager, projectionFactory);
7370
}
7471

7572
private FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Class<R> resultType,
76-
Sort sort, int limit, Collection<String> properties, BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
73+
Sort sort, int limit, Collection<String> properties, Function<FluentQuerySupport<?, ?>, TypedQuery<S>> finder,
7774
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
7875
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
7976
ProjectionFactory projectionFactory) {
@@ -101,8 +98,8 @@ public FetchableFluentQuery<R> limit(int limit) {
10198

10299
Assert.isTrue(limit >= 0, "Limit must not be negative");
103100

104-
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit,
105-
properties, finder, scroll, countOperation, existsOperation, entityManager, projectionFactory);
101+
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder,
102+
scroll, countOperation, existsOperation, entityManager, projectionFactory);
106103
}
107104

108105
@Override
@@ -155,7 +152,7 @@ public Window<R> scroll(ScrollPosition scrollPosition) {
155152

156153
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
157154

158-
return scroll.scroll(returnedType, sort, limit, scrollPosition).map(getConversionFunction());
155+
return scroll.scroll(this, scrollPosition).map(getConversionFunction());
159156
}
160157

161158
@Override
@@ -183,7 +180,7 @@ public boolean exists() {
183180

184181
private TypedQuery<S> createSortedAndProjectedQuery() {
185182

186-
TypedQuery<S> query = finder.apply(returnedType, sort);
183+
TypedQuery<S> query = finder.apply(this);
187184

188185
if (!properties.isEmpty()) {
189186
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
@@ -235,15 +232,15 @@ static class SpecificationScrollDelegate<T> extends ScrollDelegate<T> {
235232
this.scrollFunction = scrollQueryFactory;
236233
}
237234

238-
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {
235+
public Window<T> scroll(FluentQuerySupport<?, ?> q, ScrollPosition scrollPosition) {
239236

240-
Query query = scrollFunction.createQuery(returnedType, sort, scrollPosition);
237+
Query query = scrollFunction.createQuery(q, scrollPosition);
241238

242-
if (limit > 0) {
243-
query = query.setMaxResults(limit);
239+
if (q.limit > 0) {
240+
query = query.setMaxResults(q.limit);
244241
}
245242

246-
return scroll(query, sort, scrollPosition);
243+
return scroll(query, q.sort, scrollPosition);
247244
}
248245
}
249246
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ final Function<Object, R> getConversionFunction(Class<S> inputType, Class<R> tar
9595
}
9696

9797
interface ScrollQueryFactory<Q> {
98-
Q createQuery(ReturnedType returnedType, Sort sort, ScrollPosition scrollPosition);
98+
Q createQuery(FluentQuerySupport<?, ?> query, ScrollPosition scrollPosition);
9999
}
100100

101101
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,10 @@ public <S extends T, R> R findBy(Predicate predicate, Function<FetchableFluentQu
193193
return select;
194194
};
195195

196-
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scroll = (returnedType, sort, scrollPosition) -> {
196+
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scroll = (q, scrollPosition) -> {
197197

198198
Predicate predicateToUse = predicate;
199+
Sort sort = q.sort;
199200

200201
if (scrollPosition instanceof KeysetScrollPosition keyset) {
201202

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import java.util.Map;
4242
import java.util.Optional;
4343
import java.util.function.BiConsumer;
44-
import java.util.function.BiFunction;
4544
import java.util.function.Function;
4645

4746
import org.springframework.data.domain.Example;
@@ -513,17 +512,18 @@ private <S extends T, R> R doFindBy(Specification<T> spec, Class<T> domainClass,
513512
Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL);
514513
Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL);
515514

516-
ScrollQueryFactory<TypedQuery<T>> scrollFunction = (returnedType, sort, scrollPosition) -> {
515+
ScrollQueryFactory<TypedQuery<T>> scrollFunction = (q, scrollPosition) -> {
517516

518517
Specification<T> specToUse = spec;
518+
Sort sort = q.sort;
519519

520520
if (scrollPosition instanceof KeysetScrollPosition keyset) {
521521
KeysetScrollSpecification<T> keysetSpec = new KeysetScrollSpecification<>(keyset, sort, entityInformation);
522522
sort = keysetSpec.sort();
523523
specToUse = specToUse.and(keysetSpec);
524524
}
525525

526-
TypedQuery<T> query = getQuery(returnedType, specToUse, domainClass, sort, scrollPosition);
526+
TypedQuery<T> query = getQuery(q.returnedType, specToUse, domainClass, sort, q.properties, scrollPosition);
527527

528528
if (scrollPosition instanceof OffsetScrollPosition offset) {
529529
if (!offset.isInitial()) {
@@ -534,8 +534,8 @@ private <S extends T, R> R doFindBy(Specification<T> spec, Class<T> domainClass,
534534
return query;
535535
};
536536

537-
BiFunction<ReturnedType, Sort, TypedQuery<T>> finder = (returnedType, sort) -> getQuery(returnedType, spec,
538-
domainClass, sort, null);
537+
Function<FluentQuerySupport<?, ?>, TypedQuery<T>> finder = (q) -> getQuery(q.returnedType, spec, domainClass,
538+
q.sort, q.properties, null);
539539

540540
SpecificationScrollDelegate<T> scrollDelegate = new SpecificationScrollDelegate<>(scrollFunction,
541541
entityInformation);
@@ -757,7 +757,8 @@ protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Sort sort) {
757757
* @param sort must not be {@literal null}.
758758
*/
759759
protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, Class<S> domainClass, Sort sort) {
760-
return getQuery(ReturnedType.of(domainClass, domainClass, projectionFactory), spec, domainClass, sort, null);
760+
return getQuery(ReturnedType.of(domainClass, domainClass, projectionFactory), spec, domainClass, sort,
761+
Collections.emptySet(), null);
761762
}
762763

763764
/**
@@ -767,17 +768,23 @@ protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec,
767768
* @param spec can be {@literal null}.
768769
* @param domainClass must not be {@literal null}.
769770
* @param sort must not be {@literal null}.
771+
* @param inputProperties must not be {@literal null}.
772+
* @param scrollPosition must not be {@literal null}.
770773
*/
771774
private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, @Nullable Specification<S> spec,
772-
Class<S> domainClass, Sort sort, @Nullable ScrollPosition scrollPosition) {
775+
Class<S> domainClass, Sort sort, Collection<String> inputProperties, @Nullable ScrollPosition scrollPosition) {
773776

774777
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
775778
CriteriaQuery<S> query;
776779

777-
List<String> inputProperties = returnedType.getInputProperties();
780+
boolean interfaceProjection = returnedType.getReturnedType().isInterface();
781+
782+
if (returnedType.needsCustomConstruction() && (inputProperties.isEmpty() || !interfaceProjection)) {
783+
inputProperties = returnedType.getInputProperties();
784+
}
778785

779786
if (returnedType.needsCustomConstruction()) {
780-
query = (CriteriaQuery) (returnedType.getReturnedType().isInterface() ? builder.createTupleQuery()
787+
query = (CriteriaQuery) (interfaceProjection ? builder.createTupleQuery()
781788
: builder.createQuery(returnedType.getReturnedType()));
782789
} else {
783790
query = builder.createQuery(domainClass);
@@ -789,7 +796,7 @@ private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, @Nullabl
789796

790797
Collection<String> requiredSelection;
791798

792-
if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) {
799+
if (scrollPosition instanceof KeysetScrollPosition && interfaceProjection) {
793800
requiredSelection = KeysetScrollDelegate.getProjectionInputProperties(entityInformation, inputProperties, sort);
794801
} else {
795802
requiredSelection = inputProperties;

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2697,7 +2697,7 @@ void findByFluentSpecificationPage() {
26972697
assertThat(page1.getContent()).containsExactly(fourthUser);
26982698
}
26992699

2700-
@Test // GH-2274
2700+
@Test // GH-2274, GH-3716
27012701
void findByFluentSpecificationWithInterfaceBasedProjection() {
27022702

27032703
flushTestUsers();
@@ -2707,6 +2707,14 @@ void findByFluentSpecificationWithInterfaceBasedProjection() {
27072707

27082708
assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname)
27092709
.containsExactlyInAnyOrder(firstUser.getFirstname(), thirdUser.getFirstname(), fourthUser.getFirstname());
2710+
2711+
assertThat(users).extracting(UserProjectionInterfaceBased::getLastname).doesNotContainNull();
2712+
2713+
users = repository.findBy(userHasFirstnameLike("v"),
2714+
q -> q.as(UserProjectionInterfaceBased.class).project("firstname").all());
2715+
2716+
assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname).doesNotContainNull();
2717+
assertThat(users).extracting(UserProjectionInterfaceBased::getLastname).containsExactly(null, null, null);
27102718
}
27112719

27122720
@Test // GH-2327
@@ -2716,6 +2724,12 @@ void findByFluentSpecificationWithDtoProjection() {
27162724

27172725
List<UserDto> users = repository.findBy(userHasFirstnameLike("v"), q -> q.as(UserDto.class).all());
27182726

2727+
assertThat(users).extracting(UserDto::firstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
2728+
thirdUser.getFirstname(), fourthUser.getFirstname());
2729+
2730+
// project is a no-op for DTO projections as we must use the constructor as input properties
2731+
users = repository.findBy(userHasFirstnameLike("v"), q -> q.as(UserDto.class).project("lastname").all());
2732+
27192733
assertThat(users).extracting(UserDto::firstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
27202734
thirdUser.getFirstname(), fourthUser.getFirstname());
27212735
}
@@ -3467,6 +3481,8 @@ private Page<User> executeSpecWithSort(Sort sort) {
34673481

34683482
private interface UserProjectionInterfaceBased {
34693483
String getFirstname();
3484+
3485+
String getLastname();
34703486
}
34713487

34723488
record UserDto(Integer id, String firstname, String lastname, String emailAddress) {

0 commit comments

Comments
 (0)