Skip to content

Commit

Permalink
Consider project(…) properties using the fluent query API for inter…
Browse files Browse the repository at this point in the history
…face 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
  • Loading branch information
mp911de committed Jan 15, 2025
1 parent 3001f73 commit ac43ed1
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ class PredicateScrollDelegate<T> extends ScrollDelegate<T> {

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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -101,8 +98,8 @@ public FetchableFluentQuery<R> 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
Expand Down Expand Up @@ -155,7 +152,7 @@ public Window<R> 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
Expand Down Expand Up @@ -183,7 +180,7 @@ public boolean exists() {

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));
Expand Down Expand Up @@ -235,15 +232,15 @@ static class SpecificationScrollDelegate<T> extends ScrollDelegate<T> {
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ final Function<Object, R> getConversionFunction(Class<S> inputType, Class<R> tar
}

interface ScrollQueryFactory<Q> {
Q createQuery(ReturnedType returnedType, Sort sort, ScrollPosition scrollPosition);
Q createQuery(FluentQuerySupport<?, ?> query, ScrollPosition scrollPosition);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,10 @@ public <S extends T, R> R findBy(Predicate predicate, Function<FetchableFluentQu
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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,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;
Expand Down Expand Up @@ -513,17 +512,18 @@ private <S extends T, R> R doFindBy(Specification<T> spec, Class<T> domainClass,
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);
sort = keysetSpec.sort();
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()) {
Expand All @@ -534,8 +534,8 @@ private <S extends T, R> R doFindBy(Specification<T> spec, Class<T> domainClass,
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);
Expand Down Expand Up @@ -756,7 +756,8 @@ protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) {
* @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);
}

/**
Expand All @@ -766,19 +767,25 @@ protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> 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 <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) {

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

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);
Expand All @@ -790,7 +797,7 @@ private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, @Nullabl

Collection<String> requiredSelection;

if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) {
if (scrollPosition instanceof KeysetScrollPosition && interfaceProjection) {
requiredSelection = KeysetScrollDelegate.getProjectionInputProperties(entityInformation, inputProperties, sort);
} else {
requiredSelection = inputProperties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2718,7 +2718,7 @@ void findByFluentSpecificationPage() {
assertThat(page1.getContent()).containsExactly(fourthUser);
}

@Test // GH-2274
@Test // GH-2274, GH-3716
void findByFluentSpecificationWithInterfaceBasedProjection() {

flushTestUsers();
Expand All @@ -2728,6 +2728,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
Expand All @@ -2737,6 +2745,12 @@ void findByFluentSpecificationWithDtoProjection() {

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());
}
Expand Down Expand Up @@ -3497,6 +3511,8 @@ private Page<User> executeSpecWithSort(Sort sort) {

private interface UserProjectionInterfaceBased {
String getFirstname();

String getLastname();
}

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

0 comments on commit ac43ed1

Please sign in to comment.