From 83ba970465a8f6909e0368c94da6bba0c94e616c Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 25 Oct 2021 14:55:20 +0200 Subject: [PATCH] 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. --- .../FetchableFluentQueryByExample.java | 63 +++++++----- .../FetchableFluentQueryByPredicate.java | 88 +++++++++-------- .../support/FluentQuerySupport.java | 17 ++-- .../jpa/repository/support/Projector.java | 72 ++++++++++++++ .../support/QuerydslJpaPredicateExecutor.java | 22 ++--- .../repository/support/QuerydslProjector.java | 39 ++++++++ .../support/TypedQueryProjector.java | 37 +++++++ .../jpa/repository/UserRepositoryTests.java | 79 +++++++++++++++ ...QuerydslJpaPredicateExecutorUnitTests.java | 82 +++++++++++++++- .../support/QuerydslProjectorUnitTests.java | 98 +++++++++++++++++++ 10 files changed, 512 insertions(+), 85 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/repository/support/Projector.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/support/QuerydslProjector.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/support/TypedQueryProjector.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/support/QuerydslProjectorUnitTests.java diff --git a/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java index c187ccd88..7e395ca15 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java @@ -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 Result type * @author Greg Turnquist * @author Mark Paluch + * @author Jens Schauder * @since 2.6 */ -class FetchableFluentQueryByExample extends FluentQuerySupport implements FetchableFluentQuery { +class FetchableFluentQueryByExample extends FluentQuerySupport implements FetchableFluentQuery { private final Example example; private final Function> finder; @@ -57,28 +58,31 @@ class FetchableFluentQueryByExample extends FluentQuerySupport implemen private final Function, Boolean> existsOperation; private final EntityManager entityManager; private final EscapeCharacter escapeCharacter; + private final Projector> projector; public FetchableFluentQueryByExample(Example example, Function> finder, Function, Long> countOperation, Function, Boolean> existsOperation, MappingContext, ? extends PersistentProperty> context, EntityManager entityManager, EscapeCharacter escapeCharacter) { - this(example, (Class) example.getProbeType(), Sort.unsorted(), null, finder, countOperation, existsOperation, - context, entityManager, escapeCharacter); + this(example, example.getProbeType(), (Class) example.getProbeType(), Sort.unsorted(), Collections.emptySet(), + finder, countOperation, existsOperation, context, entityManager, escapeCharacter, + new TypedQueryProjector(entityManager)); } - private FetchableFluentQueryByExample(Example example, Class returnType, Sort sort, - @Nullable Collection properties, Function> finder, - Function, Long> countOperation, Function, Boolean> existsOperation, + private FetchableFluentQueryByExample(Example example, Class entityType, Class returnType, Sort sort, + Collection properties, Function> finder, Function, Long> countOperation, + Function, Boolean> existsOperation, MappingContext, ? extends PersistentProperty> context, - EntityManager entityManager, EscapeCharacter escapeCharacter) { + EntityManager entityManager, EscapeCharacter escapeCharacter, Projector> 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 extends FluentQuerySupport 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 extends FluentQuerySupport 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 extends FluentQuerySupport implemen @Override public FetchableFluentQuery project(Collection 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 extends FluentQuerySupport implemen @Override public R oneValue() { - TypedQuery limitedQuery = this.finder.apply(this.sort); + TypedQuery limitedQuery = createSortedAndProjectedQuery(); limitedQuery.setMaxResults(2); // Never need more than 2 values List results = limitedQuery.getResultList(); @@ -147,7 +154,7 @@ class FetchableFluentQueryByExample extends FluentQuerySupport implemen @Override public R firstValue() { - TypedQuery limitedQuery = this.finder.apply(this.sort); + TypedQuery limitedQuery = createSortedAndProjectedQuery(); limitedQuery.setMaxResults(1); // Never need more than 1 value List results = limitedQuery.getResultList(); @@ -162,7 +169,7 @@ class FetchableFluentQueryByExample extends FluentQuerySupport implemen @Override public List all() { - List resultList = this.finder.apply(this.sort).getResultList(); + List resultList = createSortedAndProjectedQuery().getResultList(); return convert(resultList); } @@ -183,7 +190,7 @@ class FetchableFluentQueryByExample extends FluentQuerySupport implemen @Override public Stream stream() { - return this.finder.apply(this.sort) // + return createSortedAndProjectedQuery() // .getResultStream() // .map(getConversionFunction()); } @@ -194,7 +201,7 @@ class FetchableFluentQueryByExample extends FluentQuerySupport implemen */ @Override public long count() { - return this.countOperation.apply(example); + return countOperation.apply(example); } /* @@ -203,12 +210,12 @@ class FetchableFluentQueryByExample extends FluentQuerySupport implemen */ @Override public boolean exists() { - return this.existsOperation.apply(example); + return existsOperation.apply(example); } private Page readPage(Pageable pageable) { - TypedQuery pagedQuery = this.finder.apply(this.sort); + TypedQuery pagedQuery = createSortedAndProjectedQuery(); if (pageable.isPaged()) { pagedQuery.setFirstResult((int) pageable.getOffset()); @@ -217,7 +224,15 @@ class FetchableFluentQueryByExample extends FluentQuerySupport implemen List paginatedResults = convert(pagedQuery.getResultList()); - return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.example)); + return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(example)); + } + + private TypedQuery createSortedAndProjectedQuery() { + + TypedQuery query = finder.apply(sort); + projector.apply(entityType, query, properties); + + return query; } private List convert(List resultList) { @@ -232,7 +247,7 @@ class FetchableFluentQueryByExample extends FluentQuerySupport implemen } private Function getConversionFunction() { - return getConversionFunction(this.example.getProbeType(), this.resultType); + return getConversionFunction(example.getProbeType(), resultType); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java index a5890f568..ff9c67f83 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java @@ -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 Result type * @author Greg Turnquist * @author Mark Paluch + * @author Jens Schauder * @since 2.6 */ -class FetchableFluentQueryByPredicate extends FluentQuerySupport implements FetchableFluentQuery { +class FetchableFluentQueryByPredicate extends FluentQuerySupport implements FetchableFluentQuery { private final Predicate predicate; - private final Function> finder; - private final BiFunction> pagedFinder; + private final Function> finder; + private final BiFunction> pagedFinder; private final Function countOperation; private final Function existsOperation; - private final Class entityType; + private final Projector> projector; - public FetchableFluentQueryByPredicate(Predicate predicate, Class resultType, Function> finder, - BiFunction> pagedFinder, Function countOperation, - Function existsOperation, Class entityType, - MappingContext, ? extends PersistentProperty> context) { - this(predicate, resultType, Sort.unsorted(), null, finder, pagedFinder, countOperation, existsOperation, entityType, - context); + public FetchableFluentQueryByPredicate(Predicate predicate, Class entityType, + Function> finder, BiFunction> pagedFinder, + Function countOperation, Function existsOperation, + MappingContext, ? extends PersistentProperty> context, + Projector> projector) { + this(predicate, entityType, (Class) entityType, Sort.unsorted(), Collections.emptySet(), finder, pagedFinder, + countOperation, existsOperation, context, projector); } - private FetchableFluentQueryByPredicate(Predicate predicate, Class resultType, Sort sort, - @Nullable Collection properties, Function> finder, - BiFunction> pagedFinder, Function countOperation, - Function existsOperation, Class entityType, - MappingContext, ? extends PersistentProperty> context) { + private FetchableFluentQueryByPredicate(Predicate predicate, Class entityType, Class resultType, Sort sort, + Collection properties, Function> finder, + BiFunction> pagedFinder, Function countOperation, + Function existsOperation, + MappingContext, ? extends PersistentProperty> context, + Projector> 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 extends FluentQuerySupport 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 extends FluentQuerySupport implem public FetchableFluentQuery as(Class 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 extends FluentQuerySupport implem @Override public FetchableFluentQuery project(Collection 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 extends FluentQuerySupport implem @Override public R oneValue() { - List results = this.finder.apply(this.sort) // + List results = createSortedAndProjectedQuery() // .limit(2) // Never need more than 2 values .fetch(); @@ -146,7 +149,7 @@ class FetchableFluentQueryByPredicate extends FluentQuerySupport implem @Override public R firstValue() { - List results = this.finder.apply(this.sort) // + List results = createSortedAndProjectedQuery() // .limit(1) // Never need more than 1 value .fetch(); @@ -159,9 +162,7 @@ class FetchableFluentQueryByPredicate extends FluentQuerySupport implem */ @Override public List all() { - - JPQLQuery query = this.finder.apply(this.sort); - return convert(query.fetch()); + return convert(createSortedAndProjectedQuery().fetch()); } /* @@ -180,7 +181,7 @@ class FetchableFluentQueryByPredicate extends FluentQuerySupport implem @Override public Stream stream() { - return this.finder.apply(this.sort) // + return createSortedAndProjectedQuery() // .stream() // .map(getConversionFunction()); } @@ -191,7 +192,7 @@ class FetchableFluentQueryByPredicate extends FluentQuerySupport implem */ @Override public long count() { - return this.countOperation.apply(this.predicate); + return countOperation.apply(predicate); } /* @@ -200,31 +201,38 @@ class FetchableFluentQueryByPredicate extends FluentQuerySupport 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 readPage(Pageable pageable) { - JPQLQuery pagedQuery = this.pagedFinder.apply(this.sort, pageable); + AbstractJPAQuery pagedQuery = pagedFinder.apply(sort, pageable); List paginatedResults = convert(pagedQuery.fetch()); - return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.predicate)); + return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(predicate)); } - private List convert(List resultList) { + private List convert(List resultList) { Function conversionFunction = getConversionFunction(); List 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 getConversionFunction() { - return getConversionFunction(this.entityType, this.resultType); + return getConversionFunction(entityType, resultType); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java b/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java index 0bd878d02..2f299cc9c 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java @@ -34,20 +34,22 @@ import org.springframework.lang.Nullable; * * @param The resulting type of the query. * @author Greg Turnquist + * @author Jens Schauder * @since 2.6 */ -abstract class FluentQuerySupport { +abstract class FluentQuerySupport { protected final Class resultType; protected final Sort sort; /** Properties on which the query projects. {@literal null} stands for no special projection. */ - protected final @Nullable Set properties; + protected final Set properties; protected final MappingContext, ? extends PersistentProperty> context; + protected final Class entityType; private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); FluentQuerySupport(Class resultType, Sort sort, @Nullable Collection properties, - MappingContext, ? extends PersistentProperty> context) { + MappingContext, ? extends PersistentProperty> context, Class entityType) { this.resultType = resultType; this.sort = sort; @@ -55,24 +57,23 @@ abstract class FluentQuerySupport { if (properties != null) { this.properties = new HashSet<>(properties); } else { - this.properties = null; + this.properties = new HashSet<>(); } this.context = context; + this.entityType = entityType; } final Collection mergeProperties(Collection additionalProperties) { Set newProperties = new HashSet<>(); - if (this.properties != null) { - newProperties.addAll(this.properties); - } + newProperties.addAll(properties); newProperties.addAll(additionalProperties); return Collections.unmodifiableCollection(newProperties); } @SuppressWarnings("unchecked") - final Function getConversionFunction(Class inputType, Class targetType) { + final Function getConversionFunction(Class inputType, Class targetType) { if (targetType.isAssignableFrom(inputType)) { return (Function) Function.identity(); diff --git a/src/main/java/org/springframework/data/jpa/repository/support/Projector.java b/src/main/java/org/springframework/data/jpa/repository/support/Projector.java new file mode 100644 index 000000000..482d5f01c --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/Projector.java @@ -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 the type of the query abstraction. + * @author Jens Schauder + * @since 2.6 + */ +abstract class Projector { + + private final EntityManager entityManager; + + protected Projector(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public void apply(Class domainType, Q query, Set properties) { + + if (!properties.isEmpty()) { + + final javax.persistence.EntityGraph entityGraph = entityManager.createEntityGraph(domainType); + + for (String property : properties) { + + Subgraph 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); +} diff --git a/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java b/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index 7f32ced6e..80ac35111 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java @@ -69,7 +69,7 @@ public class QuerydslJpaPredicateExecutor 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 implements QuerydslPredicateExecuto Assert.notNull(predicate, "Predicate must not be null!"); Assert.notNull(queryFunction, "Query function must not be null!"); - Function> finder = sort -> { - JPQLQuery select = createQuery(predicate).select(path); + Function> 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> pagedFinder = (sort, pageable) -> { + BiFunction> pagedFinder = (sort, pageable) -> { - JPQLQuery 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 implements QuerydslPredicateExecuto FetchableFluentQueryByPredicate 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) fluentQuery); @@ -237,7 +237,7 @@ public class QuerydslJpaPredicateExecutor 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!"); diff --git a/src/main/java/org/springframework/data/jpa/repository/support/QuerydslProjector.java b/src/main/java/org/springframework/data/jpa/repository/support/QuerydslProjector.java new file mode 100644 index 000000000..22bd98bed --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/QuerydslProjector.java @@ -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> { + + QuerydslProjector(EntityManager entityManager) { + super(entityManager); + } + + @Override + void applyEntityGraph(AbstractJPAQuery query, EntityGraph entityGraph) { + query.setHint("javax.persistence.fetchgraph", entityGraph); + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/support/TypedQueryProjector.java b/src/main/java/org/springframework/data/jpa/repository/support/TypedQueryProjector.java new file mode 100644 index 000000000..d3b15e5cf --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/TypedQueryProjector.java @@ -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> { + + public TypedQueryProjector(EntityManager entityManager) { + super(entityManager); + } + + void applyEntityGraph(TypedQuery query, EntityGraph entityGraph) { + query.setHint("javax.persistence.fetchgraph", entityGraph); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 29f03ef11..623bb41fa 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -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 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 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 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() { diff --git a/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java index 49ee60786..1df41b945 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java @@ -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 userProjections = predicateExecutor.findBy(user.firstname.contains("v"), - q -> q.as(UserProjectionInterfaceBased.class).sortBy(Sort.by("firstname")).all()); + List 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 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 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 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 getRoles(); } } diff --git a/src/test/java/org/springframework/data/jpa/repository/support/QuerydslProjectorUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/support/QuerydslProjectorUnitTests.java new file mode 100644 index 000000000..cac7e9200 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/support/QuerydslProjectorUnitTests.java @@ -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 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 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; + } +}