From 878f2e2cc7d591c7773e6d7c08b33d34120cb3b2 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Fri, 22 Oct 2021 13:28:08 +0200 Subject: [PATCH 1/4] 2329-consider-property-paths - Prepare branch --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 27f5fb3f0e..4dc150d965 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa - 2.6.0-SNAPSHOT + 2.6.0-2329-consider-property-paths-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. From 02a9c63839a889e47d5001be115a8b6c57587d92 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 25 Oct 2021 08:34:41 +0200 Subject: [PATCH 2/4] Polishing. Comments and formatting. --- .../jpa/repository/support/FluentQuerySupport.java | 1 + .../support/QuerydslJpaPredicateExecutor.java | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) 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 d8cf794577..0bd878d024 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 @@ -40,6 +40,7 @@ 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 MappingContext, ? extends PersistentProperty> context; 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 19ecfcf79a..7f32ced6e4 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 @@ -199,10 +199,16 @@ public R findBy(Predicate predicate, Function fluentQuery = new FetchableFluentQueryByPredicate<>(predicate, - entityInformation.getJavaType(), finder, pagedFinder, this::count, this::exists, - this.entityInformation.getJavaType(), - new JpaMetamodelMappingContext(Collections.singleton(this.entityManager.getMetamodel()))); + FetchableFluentQueryByPredicate fluentQuery = new FetchableFluentQueryByPredicate<>( // + predicate, // + entityInformation.getJavaType(), // + finder, // + pagedFinder, // + this::count, // + this::exists, // + this.entityInformation.getJavaType(), // + new JpaMetamodelMappingContext(Collections.singleton(this.entityManager.getMetamodel())) // + ); return queryFunction.apply((FetchableFluentQuery) fluentQuery); } From a39cb245740d0e44ff9b7391b334a5adc4da52c0 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 25 Oct 2021 14:55:20 +0200 Subject: [PATCH 3/4] 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; + } +} From 811d2f13fa43f7c12ca48647fa9335d6c7d96fd3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 29 Oct 2021 12:01:17 +0200 Subject: [PATCH 4/4] Polishing. Rename Projector to EntityGraphFactory. Remove QuerydslProjector and TypedQueryProjector. Remove MappingContext creation. See #2329 Original pull request: #2345. --- .../support/EntityGraphFactory.java | 70 ++++++++++++++++++ .../FetchableFluentQueryByExample.java | 48 ++++++------- .../FetchableFluentQueryByPredicate.java | 53 +++++++------- .../support/FluentQuerySupport.java | 11 +-- .../jpa/repository/support/Projector.java | 72 ------------------- .../support/QuerydslJpaPredicateExecutor.java | 9 +-- .../repository/support/QuerydslProjector.java | 39 ---------- .../support/SimpleJpaRepository.java | 10 +-- .../support/TypedQueryProjector.java | 37 ---------- ....java => EntityGraphFactoryUnitTests.java} | 34 +++------ 10 files changed, 134 insertions(+), 249 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java delete mode 100644 src/main/java/org/springframework/data/jpa/repository/support/Projector.java delete mode 100644 src/main/java/org/springframework/data/jpa/repository/support/QuerydslProjector.java delete mode 100644 src/main/java/org/springframework/data/jpa/repository/support/TypedQueryProjector.java rename src/test/java/org/springframework/data/jpa/repository/support/{QuerydslProjectorUnitTests.java => EntityGraphFactoryUnitTests.java} (63%) diff --git a/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java b/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java new file mode 100644 index 0000000000..a619916bf9 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java @@ -0,0 +1,70 @@ +/* + * 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; + +/** + * Factory class to create an {@link EntityGraph} from a collection of property paths. + * + * @author Jens Schauder + * @since 2.6 + */ +abstract class EntityGraphFactory { + + public static final String HINT = "javax.persistence.fetchgraph"; + + /** + * Create an {@link EntityGraph} from a collection of properties. + * + * @param domainType + * @param properties + */ + public static EntityGraph create(EntityManager entityManager, Class domainType, Set properties) { + + EntityGraph entityGraph = entityManager.createEntityGraph(domainType); + + for (String property : properties) { + + Subgraph current = null; + + for (PropertyPath path : PropertyPath.from(property, domainType)) { + + if (path.hasNext()) { + current = current == null ? entityGraph.addSubgraph(path.getSegment()) + : current.addSubgraph(path.getSegment()); + continue; + } + + if (current == null) { + entityGraph.addAttributeNodes(path.getSegment()); + } else { + current.addAttributeNodes(path.getSegment()); + + } + } + } + + return entityGraph; + } + +} 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 7e395ca15e..cb2645bfc1 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 @@ -32,9 +32,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.EscapeCharacter; -import org.springframework.data.mapping.PersistentEntity; -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.util.Assert; @@ -58,34 +55,29 @@ class FetchableFluentQueryByExample extends FluentQuerySupport imple 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, example.getProbeType(), (Class) example.getProbeType(), Sort.unsorted(), Collections.emptySet(), - finder, countOperation, existsOperation, context, entityManager, escapeCharacter, - new TypedQueryProjector(entityManager)); + finder, countOperation, existsOperation, entityManager, escapeCharacter); } 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, Projector> projector) { + EntityManager entityManager, EscapeCharacter escapeCharacter) { - super(returnType, sort, properties, context, entityType); + super(returnType, sort, properties, entityType); this.example = example; this.finder = finder; this.countOperation = countOperation; this.existsOperation = existsOperation; this.entityManager = entityManager; this.escapeCharacter = escapeCharacter; - this.projector = projector; } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#sortBy(org.springframework.data.domain.Sort) */ @@ -95,11 +87,10 @@ public FetchableFluentQuery sortBy(Sort sort) { Assert.notNull(sort, "Sort must not be null!"); return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort.and(sort), properties, finder, - countOperation, existsOperation, context, entityManager, escapeCharacter, - new TypedQueryProjector(entityManager)); + countOperation, existsOperation, entityManager, escapeCharacter); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class) */ @@ -112,11 +103,10 @@ public FetchableFluentQuery as(Class resultType) { } return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, properties, finder, - countOperation, existsOperation, context, entityManager, escapeCharacter, - new TypedQueryProjector(entityManager)); + countOperation, existsOperation, entityManager, escapeCharacter); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection) */ @@ -124,11 +114,10 @@ public FetchableFluentQuery as(Class resultType) { public FetchableFluentQuery project(Collection properties) { return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, mergeProperties(properties), - finder, countOperation, existsOperation, context, entityManager, escapeCharacter, - new TypedQueryProjector(entityManager)); + finder, countOperation, existsOperation, entityManager, escapeCharacter); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() */ @@ -147,7 +136,7 @@ public R oneValue() { return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() */ @@ -162,7 +151,7 @@ public R firstValue() { return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() */ @@ -174,7 +163,7 @@ public List all() { return convert(resultList); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) */ @@ -183,7 +172,7 @@ public Page page(Pageable pageable) { return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() */ @@ -195,7 +184,7 @@ public Stream stream() { .map(getConversionFunction()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() */ @@ -204,7 +193,7 @@ public long count() { return countOperation.apply(example); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() */ @@ -230,7 +219,10 @@ private Page readPage(Pageable pageable) { private TypedQuery createSortedAndProjectedQuery() { TypedQuery query = finder.apply(sort); - projector.apply(entityType, query, properties); + + if (!properties.isEmpty()) { + query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties)); + } return query; } 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 ff9c67f83e..f71eeacc0b 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 @@ -23,14 +23,13 @@ import java.util.function.Function; import java.util.stream.Stream; +import javax.persistence.EntityManager; + import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.mapping.PersistentEntity; -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.util.Assert; @@ -56,34 +55,32 @@ class FetchableFluentQueryByPredicate extends FluentQuerySupport imp private final BiFunction> pagedFinder; private final Function countOperation; private final Function existsOperation; - private final Projector> projector; + private final EntityManager entityManager; public FetchableFluentQueryByPredicate(Predicate predicate, Class entityType, Function> finder, BiFunction> pagedFinder, Function countOperation, Function existsOperation, - MappingContext, ? extends PersistentProperty> context, - Projector> projector) { + EntityManager entityManager) { this(predicate, entityType, (Class) entityType, Sort.unsorted(), Collections.emptySet(), finder, pagedFinder, - countOperation, existsOperation, context, projector); + countOperation, existsOperation, entityManager); } 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) { + EntityManager entityManager) { - super(resultType, sort, properties, context, entityType); + super(resultType, sort, properties, entityType); this.predicate = predicate; this.finder = finder; this.pagedFinder = pagedFinder; this.countOperation = countOperation; this.existsOperation = existsOperation; - this.projector = projector; + this.entityManager = entityManager; } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#sortBy(org.springframework.data.domain.Sort) */ @@ -93,10 +90,10 @@ public FetchableFluentQuery sortBy(Sort sort) { Assert.notNull(sort, "Sort must not be null!"); return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort.and(sort), properties, finder, - pagedFinder, countOperation, existsOperation, context, projector); + pagedFinder, countOperation, existsOperation, entityManager); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class) */ @@ -110,10 +107,10 @@ public FetchableFluentQuery as(Class resultType) { } return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, properties, finder, - pagedFinder, countOperation, existsOperation, context, projector); + pagedFinder, countOperation, existsOperation, entityManager); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection) */ @@ -121,10 +118,10 @@ public FetchableFluentQuery as(Class resultType) { public FetchableFluentQuery project(Collection properties) { return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, mergeProperties(properties), - finder, pagedFinder, countOperation, existsOperation, context, projector); + finder, pagedFinder, countOperation, existsOperation, entityManager); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() */ @@ -142,7 +139,7 @@ public R oneValue() { return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() */ @@ -156,7 +153,7 @@ public R firstValue() { return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() */ @@ -165,7 +162,7 @@ public List all() { return convert(createSortedAndProjectedQuery().fetch()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) */ @@ -174,7 +171,7 @@ public Page page(Pageable pageable) { return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() */ @@ -186,7 +183,7 @@ public Stream stream() { .map(getConversionFunction()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() */ @@ -195,7 +192,7 @@ public long count() { return countOperation.apply(predicate); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() */ @@ -206,8 +203,12 @@ public boolean exists() { private AbstractJPAQuery createSortedAndProjectedQuery() { - final AbstractJPAQuery query = finder.apply(sort); - projector.apply(entityType, query, properties); + AbstractJPAQuery query = finder.apply(sort); + + if (!properties.isEmpty()) { + query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties)); + } + return query; } 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 2f299cc9c1..ab7d58ddc9 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 @@ -23,9 +23,6 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.domain.Sort; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.lang.Nullable; @@ -41,15 +38,12 @@ 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 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, Class entityType) { + FluentQuerySupport(Class resultType, Sort sort, @Nullable Collection properties, Class entityType) { this.resultType = resultType; this.sort = sort; @@ -57,10 +51,9 @@ abstract class FluentQuerySupport { if (properties != null) { this.properties = new HashSet<>(properties); } else { - this.properties = new HashSet<>(); + this.properties = Collections.emptySet(); } - this.context = context; this.entityType = entityType; } 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 deleted file mode 100644 index 482d5f01cb..0000000000 --- a/src/main/java/org/springframework/data/jpa/repository/support/Projector.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 80ac351113..d748761ddb 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 @@ -15,7 +15,6 @@ */ package org.springframework.data.jpa.repository.support; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.BiFunction; @@ -28,7 +27,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.QSort; @@ -69,7 +67,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}. @@ -167,7 +165,7 @@ public Page findAll(Predicate predicate, Pageable pageable) { return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchCount); } - /* + /* * (non-Javadoc) * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findBy(com.querydsl.core.types.Predicate, java.util.function.Function) */ @@ -206,8 +204,7 @@ public R findBy(Predicate predicate, Function) fluentQuery); 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 deleted file mode 100644 index 22bd98bed7..0000000000 --- a/src/main/java/org/springframework/data/jpa/repository/support/QuerydslProjector.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index b69d43667f..f5f2fe1ef8 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -48,15 +48,11 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder; import org.springframework.data.jpa.domain.Specification; -import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.QueryUtils; import org.springframework.data.jpa.repository.support.QueryHints.NoHints; -import org.springframework.data.mapping.PersistentEntity; -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.data.util.ProxyUtils; @@ -94,7 +90,6 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation entityInformation; private final EntityManager em; private final PersistenceProvider provider; - private final MappingContext, ? extends PersistentProperty> context; private @Nullable CrudMethodMetadata metadata; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; @@ -113,9 +108,6 @@ public SimpleJpaRepository(JpaEntityInformation entityInformation, EntityM this.entityInformation = entityInformation; this.em = entityManager; this.provider = PersistenceProvider.fromEntityManager(entityManager); - this.context = em.getMetamodel() != null // - ? new JpaMetamodelMappingContext(Collections.singleton(em.getMetamodel())) // - : null; } /** @@ -595,7 +587,7 @@ public R findBy(Example example, Function fluentQuery = new FetchableFluentQueryByExample<>(example, finder, this::count, - this::exists, this.context, this.em, this.escapeCharacter); + this::exists, this.em, this.escapeCharacter); return queryFunction.apply(fluentQuery); } 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 deleted file mode 100644 index d3b15e5cf7..0000000000 --- a/src/main/java/org/springframework/data/jpa/repository/support/TypedQueryProjector.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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/support/QuerydslProjectorUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/support/EntityGraphFactoryUnitTests.java similarity index 63% rename from src/test/java/org/springframework/data/jpa/repository/support/QuerydslProjectorUnitTests.java rename to src/test/java/org/springframework/data/jpa/repository/support/EntityGraphFactoryUnitTests.java index cac7e92005..e0ac1ce52c 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/QuerydslProjectorUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/EntityGraphFactoryUnitTests.java @@ -16,7 +16,6 @@ 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; @@ -28,18 +27,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.querydsl.jpa.impl.AbstractJPAQuery; - /** - * Unit tests for {@link QuerydslProjector}. + * Unit tests for {@link EntityGraphFactory}. * * @author Jens Schauder */ -public class QuerydslProjectorUnitTests { +@SuppressWarnings("rawtypes") +class EntityGraphFactoryUnitTests { EntityManager em = mock(EntityManager.class); - private EntityGraph entityGraph; - private AbstractJPAQuery jpaQuery = mock(AbstractJPAQuery.class); + EntityGraph entityGraph; @BeforeEach void beforeEach() { @@ -48,21 +45,14 @@ void beforeEach() { 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")); + HashSet properties = new HashSet<>(asList("one", "two")); - new QuerydslProjector(em).apply(DummyEntity.class, jpaQuery, properties); + entityGraph = EntityGraphFactory.create(em, DummyEntity.class, properties); - verify(jpaQuery).setHint("javax.persistence.fetchgraph", entityGraph); verify(entityGraph).addAttributeNodes("one"); verify(entityGraph).addAttributeNodes("two"); } @@ -71,20 +61,18 @@ void simpleSetOfPropertiesGetRegistered() { @Test void setOfCompositePropertiesGetRegisteredPiecewise() { - final HashSet properties = new HashSet<>(asList("one.two", "eins.zwei.drei")); - - new QuerydslProjector(em).apply(DummyEntity.class, jpaQuery, properties); + HashSet properties = new HashSet<>(asList("one.two", "eins.zwei.drei")); - verify(jpaQuery).setHint("javax.persistence.fetchgraph", entityGraph); + entityGraph = EntityGraphFactory.create(em, DummyEntity.class, properties); verify(entityGraph).addSubgraph("one"); - Subgraph one = entityGraph.addSubgraph("one"); + Subgraph one = entityGraph.addSubgraph("one"); verify(one).addAttributeNodes("two"); verify(entityGraph).addSubgraph("eins"); - Subgraph eins = entityGraph.addSubgraph("eins"); + Subgraph eins = entityGraph.addSubgraph("eins"); verify(eins).addSubgraph("zwei"); - Subgraph zwei = eins.addSubgraph("zwei"); + Subgraph zwei = eins.addSubgraph("zwei"); verify(zwei).addAttributeNodes("drei"); }