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; + } +}