Skip to content

Commit ac43ed1

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 3001f73 commit ac43ed1

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

+1-1
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

+14-17
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

+1-1
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

+2-1
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

+17-10
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import java.util.Map;
3939
import java.util.Optional;
4040
import java.util.function.BiConsumer;
41-
import java.util.function.BiFunction;
4241
import java.util.function.Function;
4342

4443
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);
@@ -756,7 +756,8 @@ protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) {
756756
* @param sort must not be {@literal null}.
757757
*/
758758
protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, Class<S> domainClass, Sort sort) {
759-
return getQuery(ReturnedType.of(domainClass, domainClass, projectionFactory), spec, domainClass, sort, null);
759+
return getQuery(ReturnedType.of(domainClass, domainClass, projectionFactory), spec, domainClass, sort,
760+
Collections.emptySet(), null);
760761
}
761762

762763
/**
@@ -766,19 +767,25 @@ protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec,
766767
* @param spec can be {@literal null}.
767768
* @param domainClass must not be {@literal null}.
768769
* @param sort must not be {@literal null}.
770+
* @param inputProperties must not be {@literal null}.
771+
* @param scrollPosition must not be {@literal null}.
769772
*/
770773
private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, @Nullable Specification<S> spec,
771-
Class<S> domainClass, Sort sort, @Nullable ScrollPosition scrollPosition) {
774+
Class<S> domainClass, Sort sort, Collection<String> inputProperties, @Nullable ScrollPosition scrollPosition) {
772775

773776
Assert.notNull(spec, "Specification must not be null");
774777

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

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

780787
if (returnedType.needsCustomConstruction()) {
781-
query = (CriteriaQuery) (returnedType.getReturnedType().isInterface() ? builder.createTupleQuery()
788+
query = (CriteriaQuery) (interfaceProjection ? builder.createTupleQuery()
782789
: builder.createQuery(returnedType.getReturnedType()));
783790
} else {
784791
query = builder.createQuery(domainClass);
@@ -790,7 +797,7 @@ private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, @Nullabl
790797

791798
Collection<String> requiredSelection;
792799

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

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

+17-1
Original file line numberDiff line numberDiff line change
@@ -2718,7 +2718,7 @@ void findByFluentSpecificationPage() {
27182718
assertThat(page1.getContent()).containsExactly(fourthUser);
27192719
}
27202720

2721-
@Test // GH-2274
2721+
@Test // GH-2274, GH-3716
27222722
void findByFluentSpecificationWithInterfaceBasedProjection() {
27232723

27242724
flushTestUsers();
@@ -2728,6 +2728,14 @@ void findByFluentSpecificationWithInterfaceBasedProjection() {
27282728

27292729
assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname)
27302730
.containsExactlyInAnyOrder(firstUser.getFirstname(), thirdUser.getFirstname(), fourthUser.getFirstname());
2731+
2732+
assertThat(users).extracting(UserProjectionInterfaceBased::getLastname).doesNotContainNull();
2733+
2734+
users = repository.findBy(userHasFirstnameLike("v"),
2735+
q -> q.as(UserProjectionInterfaceBased.class).project("firstname").all());
2736+
2737+
assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname).doesNotContainNull();
2738+
assertThat(users).extracting(UserProjectionInterfaceBased::getLastname).containsExactly(null, null, null);
27312739
}
27322740

27332741
@Test // GH-2327
@@ -2737,6 +2745,12 @@ void findByFluentSpecificationWithDtoProjection() {
27372745

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

2748+
assertThat(users).extracting(UserDto::firstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
2749+
thirdUser.getFirstname(), fourthUser.getFirstname());
2750+
2751+
// project is a no-op for DTO projections as we must use the constructor as input properties
2752+
users = repository.findBy(userHasFirstnameLike("v"), q -> q.as(UserDto.class).project("lastname").all());
2753+
27402754
assertThat(users).extracting(UserDto::firstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
27412755
thirdUser.getFirstname(), fourthUser.getFirstname());
27422756
}
@@ -3497,6 +3511,8 @@ private Page<User> executeSpecWithSort(Sort sort) {
34973511

34983512
private interface UserProjectionInterfaceBased {
34993513
String getFirstname();
3514+
3515+
String getLastname();
35003516
}
35013517

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

0 commit comments

Comments
 (0)