From a39cb245740d0e44ff9b7391b334a5adc4da52c0 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 | 90 +++++++++-------- .../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, 513 insertions(+), 86 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 c187ccd884..7e395ca15e 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 @@ 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.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 @@ * @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 @@ public FetchableFluentQuery sortBy(Sort sort) { 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 @@ public FetchableFluentQuery as(Class resultType) { 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 @@ public FetchableFluentQuery as(Class resultType) { @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 @@ public FetchableFluentQuery project(Collection properties) { @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 @@ public R oneValue() { @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 @@ public R firstValue() { @Override public List all() { - List resultList = this.finder.apply(this.sort).getResultList(); + List resultList = createSortedAndProjectedQuery().getResultList(); return convert(resultList); } @@ -183,7 +190,7 @@ public Page page(Pageable pageable) { @Override public Stream stream() { - return this.finder.apply(this.sort) // + return createSortedAndProjectedQuery() // .getResultStream() // .map(getConversionFunction()); } @@ -194,7 +201,7 @@ public Stream stream() { */ @Override public long count() { - return this.countOperation.apply(example); + return countOperation.apply(example); } /* @@ -203,12 +210,12 @@ public long count() { */ @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 @@ private Page readPage(Pageable pageable) { 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 @@ private List convert(List resultList) { } 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 a5890f568f..ff9c67f83e 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 @@ 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.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 @@ * @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; - - 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); + private final Projector> projector; + + 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 @@ public FetchableFluentQuery sortBy(Sort sort) { 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 @@ public FetchableFluentQuery sortBy(Sort sort) { 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 @@ public FetchableFluentQuery as(Class resultType) { @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 @@ public FetchableFluentQuery project(Collection properties) { @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 @@ public R oneValue() { @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 @@ public R firstValue() { */ @Override public List all() { - - JPQLQuery query = this.finder.apply(this.sort); - return convert(query.fetch()); + return convert(createSortedAndProjectedQuery().fetch()); } /* @@ -180,7 +181,7 @@ public Page page(Pageable pageable) { @Override public Stream stream() { - return this.finder.apply(this.sort) // + return createSortedAndProjectedQuery() // .stream() // .map(getConversionFunction()); } @@ -191,7 +192,7 @@ public Stream stream() { */ @Override public long count() { - return this.countOperation.apply(this.predicate); + return countOperation.apply(predicate); } /* @@ -200,31 +201,38 @@ public long count() { */ @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 0bd878d024..2f299cc9c1 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 @@ * * @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 0000000000..482d5f01cb --- /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 7f32ced6e4..80ac351113 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 R findBy(Predicate predicate, 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 R findBy(Predicate predicate, Function 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 boolean exists(Predicate predicate) { * @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 0000000000..22bd98bed7 --- /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 0000000000..d3b15e5cf7 --- /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 29f03ef110..623bb41fa0 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.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 @@ void findByFluentExampleWithInterfaceBasedProjection() { .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 49ee607867..1df41b945f 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 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 @@ void findByFluentPredicateWithInterfaceBasedProjection() { @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 UserDto { .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 0000000000..cac7e92005 --- /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; + } +}