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:
committed by
Greg L. Turnquist
parent
43305f67cb
commit
83ba970465
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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!");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user