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. 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 c187ccd884..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 @@ -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; @@ -31,12 +32,8 @@ 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.lang.Nullable; import org.springframework.util.Assert; /** @@ -47,9 +44,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; @@ -60,19 +58,17 @@ class FetchableFluentQueryByExample extends FluentQuerySupport implemen 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, entityManager, escapeCharacter); } - private FetchableFluentQueryByExample(Example example, Class returnType, Sort sort, - @Nullable Collection properties, Function> finder, - Function, Long> countOperation, Function, Boolean> existsOperation, - MappingContext, ? extends PersistentProperty> context, + private FetchableFluentQueryByExample(Example example, Class entityType, Class returnType, Sort sort, + Collection properties, Function> finder, Function, Long> countOperation, + Function, Boolean> existsOperation, EntityManager entityManager, EscapeCharacter escapeCharacter) { - super(returnType, sort, properties, context); + super(returnType, sort, properties, entityType); this.example = example; this.finder = finder; this.countOperation = countOperation; @@ -81,7 +77,7 @@ private FetchableFluentQueryByExample(Example example, Class returnType, S this.escapeCharacter = escapeCharacter; } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#sortBy(org.springframework.data.domain.Sort) */ @@ -90,11 +86,11 @@ 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, entityManager, escapeCharacter); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class) */ @@ -106,29 +102,29 @@ 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, entityManager, escapeCharacter); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection) */ @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, entityManager, escapeCharacter); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() */ @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(); @@ -140,14 +136,14 @@ public R oneValue() { return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() */ @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(); @@ -155,19 +151,19 @@ public R firstValue() { return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() */ @Override public List all() { - List resultList = this.finder.apply(this.sort).getResultList(); + List resultList = createSortedAndProjectedQuery().getResultList(); return convert(resultList); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) */ @@ -176,39 +172,39 @@ public Page page(Pageable pageable) { return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() */ @Override public Stream stream() { - return this.finder.apply(this.sort) // + return createSortedAndProjectedQuery() // .getResultStream() // .map(getConversionFunction()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() */ @Override public long count() { - return this.countOperation.apply(example); + return countOperation.apply(example); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() */ @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 +213,18 @@ 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); + + if (!properties.isEmpty()) { + query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties)); + } + + return query; } private List convert(List resultList) { @@ -232,7 +239,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..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 @@ -17,26 +17,25 @@ 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; 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.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,41 +45,42 @@ * @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 EntityManager entityManager; + + public FetchableFluentQueryByPredicate(Predicate predicate, Class entityType, + Function> finder, BiFunction> pagedFinder, + Function countOperation, Function existsOperation, + EntityManager entityManager) { + this(predicate, entityType, (Class) entityType, Sort.unsorted(), Collections.emptySet(), finder, pagedFinder, + countOperation, existsOperation, entityManager); } - 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, + EntityManager entityManager) { - super(resultType, sort, properties, context); + super(resultType, sort, properties, entityType); this.predicate = predicate; this.finder = finder; this.pagedFinder = pagedFinder; this.countOperation = countOperation; this.existsOperation = existsOperation; - this.entityType = entityType; + this.entityManager = entityManager; } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#sortBy(org.springframework.data.domain.Sort) */ @@ -89,11 +89,11 @@ 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, entityManager); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class) */ @@ -101,34 +101,34 @@ 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, entityManager); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection) */ @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, entityManager); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() */ @Override public R oneValue() { - List results = this.finder.apply(this.sort) // + List results = createSortedAndProjectedQuery() // .limit(2) // Never need more than 2 values .fetch(); @@ -139,32 +139,30 @@ public R oneValue() { return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() */ @Override public R firstValue() { - List results = this.finder.apply(this.sort) // + List results = createSortedAndProjectedQuery() // .limit(1) // Never need more than 1 value .fetch(); return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() */ @Override public List all() { - - JPQLQuery query = this.finder.apply(this.sort); - return convert(query.fetch()); + return convert(createSortedAndProjectedQuery().fetch()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) */ @@ -173,58 +171,69 @@ public Page page(Pageable pageable) { return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() */ @Override public Stream stream() { - return this.finder.apply(this.sort) // + return createSortedAndProjectedQuery() // .stream() // .map(getConversionFunction()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() */ @Override public long count() { - return this.countOperation.apply(this.predicate); + return countOperation.apply(predicate); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() */ @Override public boolean exists() { - return this.existsOperation.apply(this.predicate); + return existsOperation.apply(predicate); + } + + private AbstractJPAQuery createSortedAndProjectedQuery() { + + AbstractJPAQuery query = finder.apply(sort); + + if (!properties.isEmpty()) { + query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, 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 d8cf794577..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; @@ -34,19 +31,19 @@ * * @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; - protected final @Nullable Set properties; - protected final MappingContext, ? extends PersistentProperty> context; + protected final Set properties; + protected final Class entityType; private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); - FluentQuerySupport(Class resultType, Sort sort, @Nullable Collection properties, - MappingContext, ? extends PersistentProperty> context) { + FluentQuerySupport(Class resultType, Sort sort, @Nullable Collection properties, Class entityType) { this.resultType = resultType; this.sort = sort; @@ -54,24 +51,22 @@ abstract class FluentQuerySupport { if (properties != null) { this.properties = new HashSet<>(properties); } else { - this.properties = null; + this.properties = Collections.emptySet(); } - 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/QuerydslJpaPredicateExecutor.java b/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index 19ecfcf79a..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; @@ -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) */ @@ -178,31 +176,36 @@ 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; }; - FetchableFluentQueryByPredicate 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, // + this.entityInformation.getJavaType(), // + finder, // + pagedFinder, // + this::count, // + this::exists, // + entityManager // + ); return queryFunction.apply((FetchableFluentQuery) fluentQuery); } @@ -231,7 +234,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/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/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/EntityGraphFactoryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/support/EntityGraphFactoryUnitTests.java new file mode 100644 index 0000000000..e0ac1ce52c --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/support/EntityGraphFactoryUnitTests.java @@ -0,0 +1,86 @@ +/* + * 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 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; + +/** + * Unit tests for {@link EntityGraphFactory}. + * + * @author Jens Schauder + */ +@SuppressWarnings("rawtypes") +class EntityGraphFactoryUnitTests { + + EntityManager em = mock(EntityManager.class); + EntityGraph entityGraph; + + @BeforeEach + void beforeEach() { + + entityGraph = mock(EntityGraph.class, RETURNS_DEEP_STUBS); + when(em.createEntityGraph(DummyEntity.class)).thenReturn(entityGraph); + } + + // GH-2329 + @Test + void simpleSetOfPropertiesGetRegistered() { + + HashSet properties = new HashSet<>(asList("one", "two")); + + entityGraph = EntityGraphFactory.create(em, DummyEntity.class, properties); + + verify(entityGraph).addAttributeNodes("one"); + verify(entityGraph).addAttributeNodes("two"); + } + + // GH-2329 + @Test + void setOfCompositePropertiesGetRegisteredPiecewise() { + + HashSet properties = new HashSet<>(asList("one.two", "eins.zwei.drei")); + + entityGraph = EntityGraphFactory.create(em, DummyEntity.class, properties); + + 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; + } +} 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(); } }