Translate projected properties of the fluent query API into a fetchgraph.

When a property path based projection is specified we still return the root entity.
But we do provide a fetchgraph.
The JPA implementation will (should) load only the specified attributes eagerly.
It most likely will also load all other attributes from all selected tables.

Once we have infrastructure in place for for multilevel projections the same approach can and should be used for those.
Currently this is not the case.

Closes #2329
Original pull request: #2345.
This commit is contained in:
Jens Schauder
2021-10-25 14:55:20 +02:00
committed by Greg L. Turnquist
parent 43305f67cb
commit 83ba970465
10 changed files with 512 additions and 85 deletions

View File

@@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.support;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -36,7 +37,6 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -47,9 +47,10 @@ import org.springframework.util.Assert;
* @param <R> Result type
* @author Greg Turnquist
* @author Mark Paluch
* @author Jens Schauder
* @since 2.6
*/
class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> implements FetchableFluentQuery<R> {
private final Example<S> example;
private final Function<Sort, TypedQuery<S>> finder;
@@ -57,28 +58,31 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
private final Function<Example<S>, Boolean> existsOperation;
private final EntityManager entityManager;
private final EscapeCharacter escapeCharacter;
private final Projector<TypedQuery<?>> projector;
public FetchableFluentQueryByExample(Example<S> example, Function<Sort, TypedQuery<S>> finder,
Function<Example<S>, Long> countOperation, Function<Example<S>, Boolean> existsOperation,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
EntityManager entityManager, EscapeCharacter escapeCharacter) {
this(example, (Class<R>) example.getProbeType(), Sort.unsorted(), null, finder, countOperation, existsOperation,
context, entityManager, escapeCharacter);
this(example, example.getProbeType(), (Class<R>) example.getProbeType(), Sort.unsorted(), Collections.emptySet(),
finder, countOperation, existsOperation, context, entityManager, escapeCharacter,
new TypedQueryProjector(entityManager));
}
private FetchableFluentQueryByExample(Example<S> example, Class<R> returnType, Sort sort,
@Nullable Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
Function<Example<S>, Long> countOperation, Function<Example<S>, Boolean> existsOperation,
private FetchableFluentQueryByExample(Example<S> example, Class<S> entityType, Class<R> returnType, Sort sort,
Collection<String> properties, Function<Sort, TypedQuery<S>> finder, Function<Example<S>, Long> countOperation,
Function<Example<S>, Boolean> existsOperation,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
EntityManager entityManager, EscapeCharacter escapeCharacter) {
EntityManager entityManager, EscapeCharacter escapeCharacter, Projector<TypedQuery<?>> projector) {
super(returnType, sort, properties, context);
super(returnType, sort, properties, context, entityType);
this.example = example;
this.finder = finder;
this.countOperation = countOperation;
this.existsOperation = existsOperation;
this.entityManager = entityManager;
this.escapeCharacter = escapeCharacter;
this.projector = projector;
}
/*
@@ -90,8 +94,9 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
Assert.notNull(sort, "Sort must not be null!");
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.sort.and(sort), this.properties,
this.finder, this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort.and(sort), properties, finder,
countOperation, existsOperation, context, entityManager, escapeCharacter,
new TypedQueryProjector(entityManager));
}
/*
@@ -106,8 +111,9 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
}
return new FetchableFluentQueryByExample<>(this.example, resultType, this.sort, this.properties, this.finder,
this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, properties, finder,
countOperation, existsOperation, context, entityManager, escapeCharacter,
new TypedQueryProjector(entityManager));
}
/*
@@ -117,8 +123,9 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
@Override
public FetchableFluentQuery<R> project(Collection<String> properties) {
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.sort, mergeProperties(properties),
this.finder, this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, mergeProperties(properties),
finder, countOperation, existsOperation, context, entityManager, escapeCharacter,
new TypedQueryProjector(entityManager));
}
/*
@@ -128,7 +135,7 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
@Override
public R oneValue() {
TypedQuery<S> limitedQuery = this.finder.apply(this.sort);
TypedQuery<S> limitedQuery = createSortedAndProjectedQuery();
limitedQuery.setMaxResults(2); // Never need more than 2 values
List<S> results = limitedQuery.getResultList();
@@ -147,7 +154,7 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
@Override
public R firstValue() {
TypedQuery<S> limitedQuery = this.finder.apply(this.sort);
TypedQuery<S> limitedQuery = createSortedAndProjectedQuery();
limitedQuery.setMaxResults(1); // Never need more than 1 value
List<S> results = limitedQuery.getResultList();
@@ -162,7 +169,7 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
@Override
public List<R> all() {
List<S> resultList = this.finder.apply(this.sort).getResultList();
List<S> resultList = createSortedAndProjectedQuery().getResultList();
return convert(resultList);
}
@@ -183,7 +190,7 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
@Override
public Stream<R> stream() {
return this.finder.apply(this.sort) //
return createSortedAndProjectedQuery() //
.getResultStream() //
.map(getConversionFunction());
}
@@ -194,7 +201,7 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
*/
@Override
public long count() {
return this.countOperation.apply(example);
return countOperation.apply(example);
}
/*
@@ -203,12 +210,12 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
*/
@Override
public boolean exists() {
return this.existsOperation.apply(example);
return existsOperation.apply(example);
}
private Page<R> readPage(Pageable pageable) {
TypedQuery<S> pagedQuery = this.finder.apply(this.sort);
TypedQuery<S> pagedQuery = createSortedAndProjectedQuery();
if (pageable.isPaged()) {
pagedQuery.setFirstResult((int) pageable.getOffset());
@@ -217,7 +224,15 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
List<R> paginatedResults = convert(pagedQuery.getResultList());
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.example));
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(example));
}
private TypedQuery<S> createSortedAndProjectedQuery() {
TypedQuery<S> query = finder.apply(sort);
projector.apply(entityType, query, properties);
return query;
}
private List<R> convert(List<S> resultList) {
@@ -232,7 +247,7 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implemen
}
private Function<Object, R> getConversionFunction() {
return getConversionFunction(this.example.getProbeType(), this.resultType);
return getConversionFunction(example.getProbeType(), resultType);
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.support;
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;
@@ -32,11 +33,10 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.AbstractJPAQuery;
/**
* Immutable implementation of {@link FetchableFluentQuery} based on a Querydsl {@link Predicate}. All methods that
@@ -46,38 +46,41 @@ import com.querydsl.jpa.JPQLQuery;
* @param <R> Result type
* @author Greg Turnquist
* @author Mark Paluch
* @author Jens Schauder
* @since 2.6
*/
class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> implements FetchableFluentQuery<R> {
private final Predicate predicate;
private final Function<Sort, JPQLQuery<S>> finder;
private final BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder;
private final Function<Sort, AbstractJPAQuery<?, ?>> finder;
private final BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder;
private final Function<Predicate, Long> countOperation;
private final Function<Predicate, Boolean> existsOperation;
private final Class<S> entityType;
private final Projector<AbstractJPAQuery<?, ?>> projector;
public FetchableFluentQueryByPredicate(Predicate predicate, Class<R> resultType, Function<Sort, JPQLQuery<S>> finder,
BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder, Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation, Class<S> entityType,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context) {
this(predicate, resultType, Sort.unsorted(), null, finder, pagedFinder, countOperation, existsOperation, entityType,
context);
public FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType,
Function<Sort, AbstractJPAQuery<?, ?>> finder, BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
Function<Predicate, Long> countOperation, Function<Predicate, Boolean> existsOperation,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
Projector<AbstractJPAQuery<?, ?>> projector) {
this(predicate, entityType, (Class<R>) entityType, Sort.unsorted(), Collections.emptySet(), finder, pagedFinder,
countOperation, existsOperation, context, projector);
}
private FetchableFluentQueryByPredicate(Predicate predicate, Class<R> resultType, Sort sort,
@Nullable Collection<String> properties, Function<Sort, JPQLQuery<S>> finder,
BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder, Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation, Class<S> entityType,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context) {
private FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType, Class<R> resultType, Sort sort,
Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder, Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
Projector<AbstractJPAQuery<?, ?>> projector) {
super(resultType, sort, properties, context);
super(resultType, sort, properties, context, entityType);
this.predicate = predicate;
this.finder = finder;
this.pagedFinder = pagedFinder;
this.countOperation = countOperation;
this.existsOperation = existsOperation;
this.entityType = entityType;
this.projector = projector;
}
/*
@@ -89,8 +92,8 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implem
Assert.notNull(sort, "Sort must not be null!");
return new FetchableFluentQueryByPredicate<>(this.predicate, this.resultType, this.sort.and(sort), this.properties,
this.finder, this.pagedFinder, this.countOperation, this.existsOperation, this.entityType, this.context);
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort.and(sort), properties, finder,
pagedFinder, countOperation, existsOperation, context, projector);
}
/*
@@ -101,12 +104,13 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implem
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
Assert.notNull(resultType, "Projection target type must not be null!");
if (!resultType.isInterface()) {
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
}
return new FetchableFluentQueryByPredicate<>(this.predicate, resultType, this.sort, this.properties, this.finder,
this.pagedFinder, this.countOperation, this.existsOperation, this.entityType, this.context);
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, properties, finder,
pagedFinder, countOperation, existsOperation, context, projector);
}
/*
@@ -116,9 +120,8 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implem
@Override
public FetchableFluentQuery<R> project(Collection<String> properties) {
return new FetchableFluentQueryByPredicate<>(this.predicate, this.resultType, this.sort,
mergeProperties(properties), this.finder, this.pagedFinder, this.countOperation, this.existsOperation,
this.entityType, this.context);
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, mergeProperties(properties),
finder, pagedFinder, countOperation, existsOperation, context, projector);
}
/*
@@ -128,7 +131,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implem
@Override
public R oneValue() {
List<S> results = this.finder.apply(this.sort) //
List<?> results = createSortedAndProjectedQuery() //
.limit(2) // Never need more than 2 values
.fetch();
@@ -146,7 +149,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implem
@Override
public R firstValue() {
List<S> results = this.finder.apply(this.sort) //
List<?> results = createSortedAndProjectedQuery() //
.limit(1) // Never need more than 1 value
.fetch();
@@ -159,9 +162,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implem
*/
@Override
public List<R> all() {
JPQLQuery<S> query = this.finder.apply(this.sort);
return convert(query.fetch());
return convert(createSortedAndProjectedQuery().fetch());
}
/*
@@ -180,7 +181,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implem
@Override
public Stream<R> stream() {
return this.finder.apply(this.sort) //
return createSortedAndProjectedQuery() //
.stream() //
.map(getConversionFunction());
}
@@ -191,7 +192,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implem
*/
@Override
public long count() {
return this.countOperation.apply(this.predicate);
return countOperation.apply(predicate);
}
/*
@@ -200,31 +201,38 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implem
*/
@Override
public boolean exists() {
return this.existsOperation.apply(this.predicate);
return existsOperation.apply(predicate);
}
private AbstractJPAQuery<?, ?> createSortedAndProjectedQuery() {
final AbstractJPAQuery<?, ?> query = finder.apply(sort);
projector.apply(entityType, query, properties);
return query;
}
private Page<R> readPage(Pageable pageable) {
JPQLQuery<S> pagedQuery = this.pagedFinder.apply(this.sort, pageable);
AbstractJPAQuery<?, ?> pagedQuery = pagedFinder.apply(sort, pageable);
List<R> paginatedResults = convert(pagedQuery.fetch());
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.predicate));
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(predicate));
}
private List<R> convert(List<S> resultList) {
private List<R> convert(List<?> resultList) {
Function<Object, R> conversionFunction = getConversionFunction();
List<R> mapped = new ArrayList<>(resultList.size());
for (S s : resultList) {
mapped.add(conversionFunction.apply(s));
for (Object o : resultList) {
mapped.add(conversionFunction.apply(o));
}
return mapped;
}
private Function<Object, R> getConversionFunction() {
return getConversionFunction(this.entityType, this.resultType);
return getConversionFunction(entityType, resultType);
}
}

View File

@@ -34,20 +34,22 @@ import org.springframework.lang.Nullable;
*
* @param <R> The resulting type of the query.
* @author Greg Turnquist
* @author Jens Schauder
* @since 2.6
*/
abstract class FluentQuerySupport<R> {
abstract class FluentQuerySupport<S, R> {
protected final Class<R> resultType;
protected final Sort sort;
/** Properties on which the query projects. {@literal null} stands for no special projection. */
protected final @Nullable Set<String> properties;
protected final Set<String> properties;
protected final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context;
protected final Class<S> entityType;
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
FluentQuerySupport(Class<R> resultType, Sort sort, @Nullable Collection<String> properties,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context) {
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context, Class<S> entityType) {
this.resultType = resultType;
this.sort = sort;
@@ -55,24 +57,23 @@ abstract class FluentQuerySupport<R> {
if (properties != null) {
this.properties = new HashSet<>(properties);
} else {
this.properties = null;
this.properties = new HashSet<>();
}
this.context = context;
this.entityType = entityType;
}
final Collection<String> mergeProperties(Collection<String> additionalProperties) {
Set<String> newProperties = new HashSet<>();
if (this.properties != null) {
newProperties.addAll(this.properties);
}
newProperties.addAll(properties);
newProperties.addAll(additionalProperties);
return Collections.unmodifiableCollection(newProperties);
}
@SuppressWarnings("unchecked")
final <S> Function<Object, R> getConversionFunction(Class<S> inputType, Class<R> targetType) {
final Function<Object, R> getConversionFunction(Class<S> inputType, Class<R> targetType) {
if (targetType.isAssignableFrom(inputType)) {
return (Function<Object, R>) Function.identity();

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import java.util.Set;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.Subgraph;
import org.springframework.data.mapping.PropertyPath;
/**
* Turns a collection of property paths to an {@link EntityGraph} and applies it to a query abstraction
*
* @param <Q> the type of the query abstraction.
* @author Jens Schauder
* @since 2.6
*/
abstract class Projector<Q> {
private final EntityManager entityManager;
protected Projector(EntityManager entityManager) {
this.entityManager = entityManager;
}
public void apply(Class<?> domainType, Q query, Set<String> properties) {
if (!properties.isEmpty()) {
final javax.persistence.EntityGraph<?> entityGraph = entityManager.createEntityGraph(domainType);
for (String property : properties) {
Subgraph<Object> subgraph = null;
for (PropertyPath path : PropertyPath.from(property, domainType)) {
if (path.hasNext()) {
subgraph = subgraph == null ? entityGraph.addSubgraph(path.getSegment())
: subgraph.addSubgraph(path.getSegment());
} else {
if (subgraph == null) {
entityGraph.addAttributeNodes(path.getSegment());
} else {
subgraph.addAttributeNodes(path.getSegment());
}
}
}
}
applyEntityGraph(query, entityGraph);
}
}
abstract void applyEntityGraph(Q query, EntityGraph<?> entityGraph);
}

View File

@@ -69,7 +69,7 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
/**
* Creates a new {@link QuerydslJpaPredicateExecutor} from the given domain class and {@link EntityManager} and uses
* the given {@link EntityPathResolver} to translate the domain class into an {@link EntityPath}.
*
*
* @param entityInformation must not be {@literal null}.
* @param entityManager must not be {@literal null}.
* @param resolver must not be {@literal null}.
@@ -178,22 +178,22 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
Assert.notNull(predicate, "Predicate must not be null!");
Assert.notNull(queryFunction, "Query function must not be null!");
Function<Sort, JPQLQuery<T>> finder = sort -> {
JPQLQuery<T> select = createQuery(predicate).select(path);
Function<Sort, AbstractJPAQuery<?, ?>> finder = sort -> {
AbstractJPAQuery<?, ?> select = (AbstractJPAQuery<?, ?>) createQuery(predicate).select(path);
if (sort != null) {
select = querydsl.applySorting(sort, select);
select = (AbstractJPAQuery<?, ?>) querydsl.applySorting(sort, select);
}
return select;
};
BiFunction<Sort, Pageable, JPQLQuery<T>> pagedFinder = (sort, pageable) -> {
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder = (sort, pageable) -> {
JPQLQuery<T> select = finder.apply(sort);
AbstractJPAQuery<?, ?> select = finder.apply(sort);
if (pageable.isPaged()) {
select = querydsl.applyPagination(pageable, select);
select = (AbstractJPAQuery<?, ?>) querydsl.applyPagination(pageable, select);
}
return select;
@@ -201,13 +201,13 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
FetchableFluentQueryByPredicate<T, T> fluentQuery = new FetchableFluentQueryByPredicate<>( //
predicate, //
entityInformation.getJavaType(), //
this.entityInformation.getJavaType(), //
finder, //
pagedFinder, //
this::count, //
this::exists, //
this.entityInformation.getJavaType(), //
new JpaMetamodelMappingContext(Collections.singleton(this.entityManager.getMetamodel())) //
new JpaMetamodelMappingContext(Collections.singleton(this.entityManager.getMetamodel())), //
new QuerydslProjector(entityManager) //
);
return queryFunction.apply((FetchableFluentQuery<S>) fluentQuery);
@@ -237,7 +237,7 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
* @param predicate
* @return the Querydsl {@link JPQLQuery}.
*/
protected JPQLQuery<?> createQuery(Predicate... predicate) {
protected AbstractJPAQuery<?, ?> createQuery(Predicate... predicate) {
Assert.notNull(predicate, "Predicate must not be null!");

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import com.querydsl.jpa.impl.AbstractJPAQuery;
/**
* Applies fetchgraph hints to {@code AbstractJPAQuery}.
*
* @author Jens Schauder
* @since 2.6
*/
class QuerydslProjector extends Projector<AbstractJPAQuery<?, ?>> {
QuerydslProjector(EntityManager entityManager) {
super(entityManager);
}
@Override
void applyEntityGraph(AbstractJPAQuery<?, ?> query, EntityGraph<?> entityGraph) {
query.setHint("javax.persistence.fetchgraph", entityGraph);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
/**
* Applies fetchgraph hints to {@code TypedQuery}.
*
* @author Jens Schauder
* @since 2.6
*/
public class TypedQueryProjector extends Projector<TypedQuery<?>> {
public TypedQueryProjector(EntityManager entityManager) {
super(entityManager);
}
void applyEntityGraph(TypedQuery<?> query, EntityGraph<?> entityGraph) {
query.setHint("javax.persistence.fetchgraph", entityGraph);
}
}

View File

@@ -45,6 +45,7 @@ import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.assertj.core.api.SoftAssertions;
import org.hibernate.LazyInitializationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -2138,6 +2139,84 @@ public class UserRepositoryTests {
.containsExactlyInAnyOrder(firstUser.getFirstname(), thirdUser.getFirstname(), fourthUser.getFirstname());
}
@Test // GH-2294
void findByFluentExampleWithSimplePropertyPathsDoesntLoadUnrequestedPaths() {
flushTestUsers();
// make sure we don't get preinitialized entities back:
em.clear();
User prototype = new User();
prototype.setFirstname("v");
List<User> users = repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.project("firstname").all());
// remove the entities, so lazy loading throws an exception
em.clear();
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
thirdUser.getFirstname(), fourthUser.getFirstname());
assertThatExceptionOfType(LazyInitializationException.class) //
.isThrownBy( //
() -> users.forEach(u -> u.getRoles().size()) // forces loading of roles
);
}
@Test // GH-2294
void findByFluentExampleWithCollectionPropertyPathsDoesntLoadUnrequestedPaths() {
flushTestUsers();
// make sure we don't get preinitialized entities back:
em.clear();
User prototype = new User();
prototype.setFirstname("v");
List<User> users = repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.project("firstname", "roles").all());
// remove the entities, so lazy loading throws an exception
em.clear();
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
thirdUser.getFirstname(), fourthUser.getFirstname());
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
}
@Test // GH-2294
void findByFluentExampleWithComplexPropertyPathsDoesntLoadUnrequestedPaths() {
flushTestUsers();
// make sure we don't get preinitialized entities back:
em.clear();
User prototype = new User();
prototype.setFirstname("v");
List<User> users = repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.project("roles.name").all());
// remove the entities, so lazy loading throws an exception
em.clear();
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
thirdUser.getFirstname(), fourthUser.getFirstname());
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
}
@Test // GH-2294
void findByFluentExampleWithSortedInterfaceBasedProjection() {

View File

@@ -22,11 +22,13 @@ import lombok.Data;
import java.sql.Date;
import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.hibernate.LazyInitializationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -405,8 +407,12 @@ class QuerydslJpaPredicateExecutorUnitTests {
@Test // GH-2294
void findByFluentPredicateWithSortedInterfaceBasedProjection() {
List<UserProjectionInterfaceBased> userProjections = predicateExecutor.findBy(user.firstname.contains("v"),
q -> q.as(UserProjectionInterfaceBased.class).sortBy(Sort.by("firstname")).all());
List<UserProjectionInterfaceBased> userProjections = predicateExecutor.findBy( //
user.firstname.contains("v"), //
q -> q.as(UserProjectionInterfaceBased.class) //
.sortBy(Sort.by("firstname")) //
.all() //
);
assertThat(userProjections).extracting(UserProjectionInterfaceBased::getFirstname)
.containsExactly(dave.getFirstname(), oliver.getFirstname());
@@ -442,7 +448,79 @@ class QuerydslJpaPredicateExecutorUnitTests {
.findBy(user.firstname.contains("v"), q -> q.as(UserDto.class).sortBy(Sort.by("firstname")).all()));
}
@Test // GH-2329
void findByFluentPredicateWithSimplePropertyPathsDoesntLoadUnrequestedPaths() {
// make sure the entities are actually written to the database:
em.flush();
// make sure we don't get preinitialized entities back:
em.clear();
List<User> users = predicateExecutor.findBy(user.firstname.contains("v"),
q -> q.project("firstname", "lastname").all());
// remove the entities, so lazy loading throws an exception
em.clear();
assertThat(users).extracting(User::getFirstname) //
.containsExactlyInAnyOrder( //
dave.getFirstname(), //
oliver.getFirstname() //
);
assertThatExceptionOfType(LazyInitializationException.class) //
.isThrownBy( //
() -> users.forEach(u -> u.getRoles().size()) // forces loading of roles
);
}
@Test // GH-2329
void findByFluentPredicateWithCollectionPropertyPathsLoadsRequestedPaths() {
// make sure the entities are actually written to the database:
em.flush();
// make sure we don't get preinitialized entities back:
em.clear();
List<User> users = predicateExecutor.findBy(user.firstname.contains("v"),
q -> q.project("firstname", "roles").all());
// remove the entities, so lazy loading throws an exception
em.clear();
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder( //
dave.getFirstname(), //
oliver.getFirstname() //
);
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
}
@Test // GH-2329
void findByFluentPredicateWithComplexPropertyPathsDoesntLoadsRequestedPaths() {
// make sure the entities are actually written to the database:
em.flush();
// make sure we don't get preinitialized entities back:
em.clear();
List<User> users = predicateExecutor.findBy(user.firstname.contains("v"), q -> q.project("roles.name").all());
// remove the entities, so lazy loading throws an exception
em.clear();
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder( //
dave.getFirstname(), //
oliver.getFirstname() //
);
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
}
private interface UserProjectionInterfaceBased {
String getFirstname();
Set<Role> getRoles();
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static org.mockito.Mockito.*;
import java.util.HashSet;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.Subgraph;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.querydsl.jpa.impl.AbstractJPAQuery;
/**
* Unit tests for {@link QuerydslProjector}.
*
* @author Jens Schauder
*/
public class QuerydslProjectorUnitTests {
EntityManager em = mock(EntityManager.class);
private EntityGraph entityGraph;
private AbstractJPAQuery jpaQuery = mock(AbstractJPAQuery.class);
@BeforeEach
void beforeEach() {
entityGraph = mock(EntityGraph.class, RETURNS_DEEP_STUBS);
when(em.createEntityGraph(DummyEntity.class)).thenReturn(entityGraph);
}
// GH-2329
@Test
void emptySetOfPropertiesDoesNotCreateEntityGraph() {
new QuerydslProjector(em).apply(DummyEntity.class, jpaQuery, emptySet());
}
// GH-2329
@Test
void simpleSetOfPropertiesGetRegistered() {
final HashSet<String> properties = new HashSet<>(asList("one", "two"));
new QuerydslProjector(em).apply(DummyEntity.class, jpaQuery, properties);
verify(jpaQuery).setHint("javax.persistence.fetchgraph", entityGraph);
verify(entityGraph).addAttributeNodes("one");
verify(entityGraph).addAttributeNodes("two");
}
// GH-2329
@Test
void setOfCompositePropertiesGetRegisteredPiecewise() {
final HashSet<String> properties = new HashSet<>(asList("one.two", "eins.zwei.drei"));
new QuerydslProjector(em).apply(DummyEntity.class, jpaQuery, properties);
verify(jpaQuery).setHint("javax.persistence.fetchgraph", entityGraph);
verify(entityGraph).addSubgraph("one");
Subgraph one = entityGraph.addSubgraph("one");
verify(one).addAttributeNodes("two");
verify(entityGraph).addSubgraph("eins");
Subgraph eins = entityGraph.addSubgraph("eins");
verify(eins).addSubgraph("zwei");
Subgraph zwei = eins.addSubgraph("zwei");
verify(zwei).addAttributeNodes("drei");
}
private static class DummyEntity {
DummyEntity one;
DummyEntity two;
DummyEntity eins;
DummyEntity zwei;
DummyEntity drei;
}
}