From 64beaab1e41ac9fa321d581e092331b4d07db8f0 Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Thu, 16 Sep 2021 14:04:25 -0500 Subject: [PATCH] Implement FluentQuery for Querydsl and Query by Example. Add support for both QueryByExampleExecutor and QuerydslPredicateExecutor. This is used in SimpleJpaRepository and QuerydslJpaPredicateExecutor, resulting in various test cases proving support by both examples and Querydsl predicates. NOTE: Class-based DTOs are NOT supported yet. Closes #2294. Related: #2327. --- pom.xml | 2 +- .../jpa/repository/query/JpaQueryCreator.java | 10 +- .../FetchableFluentQueryByExample.java | 183 +++++++++++++++++ .../FetchableFluentQueryByPredicate.java | 173 ++++++++++++++++ .../support/FluentQuerySupport.java | 86 ++++++++ .../support/QuerydslJpaPredicateExecutor.java | 53 ++++- .../support/QuerydslJpaRepository.java | 12 +- .../support/SimpleJpaRepository.java | 42 +++- .../jpa/repository/UserRepositoryTests.java | 189 +++++++++++++++++- ...QuerydslJpaPredicateExecutorUnitTests.java | 133 +++++++++++- .../support/QuerydslJpaRepositoryTests.java | 16 +- .../support/SimpleJpaRepositoryUnitTests.java | 4 +- 12 files changed, 875 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java diff --git a/pom.xml b/pom.xml index dd472ffbe2..ec572e66ae 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ 5.5.3.Final 8.0.23 42.2.19 - 2.6.0-SNAPSHOT + 2.6.0-2228-SNAPSHOT 0.10.3 org.hibernate diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 6c9407852e..825d8a3415 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -144,8 +144,8 @@ protected Predicate or(Predicate base, Predicate predicate) { /** * Finalizes the given {@link Predicate} and applies the given sort. Delegates to - * {@link #complete(Predicate, Sort, CriteriaQuery, CriteriaBuilder, Root)} and hands it the current {@link CriteriaQuery} - * and {@link CriteriaBuilder}. + * {@link #complete(Predicate, Sort, CriteriaQuery, CriteriaBuilder, Root)} and hands it the current + * {@link CriteriaQuery} and {@link CriteriaBuilder}. */ @Override protected final CriteriaQuery complete(Predicate predicate, Sort sort) { @@ -271,10 +271,12 @@ public Predicate build() { return getTypedPath(root, part).isNotNull(); case NOT_IN: // cast required for eclipselink workaround, see DATAJPA-433 - return upperIfIgnoreCase(getTypedPath(root, part)).in((Expression>) provider.next(part, Collection.class).getExpression()).not(); + return upperIfIgnoreCase(getTypedPath(root, part)) + .in((Expression>) provider.next(part, Collection.class).getExpression()).not(); case IN: // cast required for eclipselink workaround, see DATAJPA-433 - return upperIfIgnoreCase(getTypedPath(root, part)).in((Expression>) provider.next(part, Collection.class).getExpression()); + return upperIfIgnoreCase(getTypedPath(root, part)) + .in((Expression>) provider.next(part, Collection.class).getExpression()); case STARTING_WITH: case ENDING_WITH: case CONTAINING: 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 new file mode 100644 index 0000000000..798f4663e5 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java @@ -0,0 +1,183 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.domain.Example; +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.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; + +/** + * Immutable implementation of {@link FetchableFluentQuery} based on Query by {@link Example}. All methods that return a + * {@link FetchableFluentQuery} will return a new instance, not the original. + * + * @param Domain type + * @param Result type + * @author Greg Turnquist + * @since 2.6 + */ +class FetchableFluentQueryByExample extends FluentQuerySupport implements FetchableFluentQuery { + + private final Example example; + private final Function> finder; + private final Function, Long> countOperation; + private final Function, Boolean> existsOperation; + private final EntityManager entityManager; + private final EscapeCharacter escapeCharacter; + + 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); + } + + private FetchableFluentQueryByExample(Example example, Class returnType, Sort sort, + @Nullable Collection properties, Function> finder, + Function, Long> countOperation, Function, Boolean> existsOperation, + MappingContext, ? extends PersistentProperty> context, + EntityManager entityManager, EscapeCharacter escapeCharacter) { + + super(returnType, sort, properties, context); + this.example = example; + this.finder = finder; + this.countOperation = countOperation; + this.existsOperation = existsOperation; + this.entityManager = entityManager; + this.escapeCharacter = escapeCharacter; + } + + @Override + public FetchableFluentQuery sortBy(Sort sort) { + + 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); + } + + @Override + public FetchableFluentQuery as(Class resultType) { + + if (!resultType.isInterface()) { + 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); + } + + @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); + } + + @Override + public R oneValue() { + + TypedQuery limitedQuery = this.finder.apply(this.sort); + limitedQuery.setMaxResults(2); // Never need more than 2 values + + List results = limitedQuery // + .getResultStream() // + .map(getConversionFunction(this.example.getProbeType(), this.resultType)) // + .collect(Collectors.toList()); + ; + + if (results.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + + return results.isEmpty() ? null : results.get(0); + } + + @Override + public R firstValue() { + + TypedQuery limitedQuery = this.finder.apply(this.sort); + limitedQuery.setMaxResults(1); // Never need more than 1 value + + List results = limitedQuery // + .getResultStream() // + .map(getConversionFunction(this.example.getProbeType(), this.resultType)) // + .collect(Collectors.toList()); + + return results.isEmpty() ? null : results.get(0); + } + + @Override + public List all() { + return stream().collect(Collectors.toList()); + } + + @Override + public Page page(Pageable pageable) { + return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); + } + + @Override + public Stream stream() { + + return this.finder.apply(this.sort) // + .getResultStream() // + .map(getConversionFunction(this.example.getProbeType(), this.resultType)); + } + + @Override + public long count() { + return this.countOperation.apply(example); + } + + @Override + public boolean exists() { + return this.existsOperation.apply(example); + } + + private Page readPage(Pageable pageable) { + + TypedQuery pagedQuery = this.finder.apply(this.sort); + + if (pageable.isPaged()) { + pagedQuery.setFirstResult((int) pageable.getOffset()); + pagedQuery.setMaxResults(pageable.getPageSize()); + } + + List paginatedResults = pagedQuery.getResultStream() // + .map(getConversionFunction(this.example.getProbeType(), this.resultType)) // + .collect(Collectors.toList()); + + return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.example)); + } +} 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 new file mode 100644 index 0000000000..ba2b0790fc --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java @@ -0,0 +1,173 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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 com.querydsl.core.types.Predicate; +import com.querydsl.jpa.JPQLQuery; + +/** + * Immutable implementation of {@link FetchableFluentQuery} based on a Querydsl {@link Predicate}. All methods that + * return a {@link FetchableFluentQuery} will return a new instance, not the original. + * + * @param Domain type + * @param Result type + * @author Greg Turnquist + * @since 2.6 + */ +class FetchableFluentQueryByPredicate extends FluentQuerySupport implements FetchableFluentQuery { + + private final Predicate predicate; + 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 FetchableFluentQueryByPredicate(Predicate predicate, Class resultType, Sort sort, + @Nullable Collection properties, Function> finder, + BiFunction> pagedFinder, Function countOperation, + Function existsOperation, Class entityType, + MappingContext, ? extends PersistentProperty> context) { + + super(resultType, sort, properties, context); + this.predicate = predicate; + this.finder = finder; + this.pagedFinder = pagedFinder; + this.countOperation = countOperation; + this.existsOperation = existsOperation; + this.entityType = entityType; + } + + @Override + public FetchableFluentQuery sortBy(Sort sort) { + + 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); + } + + @Override + public FetchableFluentQuery as(Class resultType) { + + 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); + } + + @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); + } + + @Override + public R oneValue() { + + List results = this.finder.apply(this.sort) // + .limit(2) // Never need more than 2 values + .stream() // + .map(getConversionFunction(this.entityType, this.resultType)) // + .collect(Collectors.toList()); + + if (results.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + + return results.isEmpty() ? null : results.get(0); + } + + @Override + public R firstValue() { + + List results = this.finder.apply(this.sort) // + .limit(1) // Never need more than 1 value + .stream() // + .map(getConversionFunction(this.entityType, this.resultType)) // + .collect(Collectors.toList()); + + return results.isEmpty() ? null : results.get(0); + } + + @Override + public List all() { + return stream().collect(Collectors.toList()); + } + + @Override + public Page page(Pageable pageable) { + return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); + } + + @Override + public Stream stream() { + + return this.finder.apply(this.sort) // + .stream() // + .map(getConversionFunction(this.entityType, this.resultType)); + } + + @Override + public long count() { + return this.countOperation.apply(this.predicate); + } + + @Override + public boolean exists() { + return this.existsOperation.apply(this.predicate); + } + + private Page readPage(Pageable pageable) { + + JPQLQuery pagedQuery = this.pagedFinder.apply(this.sort, pageable); + + List paginatedResults = pagedQuery.stream() // + .map(getConversionFunction(this.entityType, this.resultType)) // + .collect(Collectors.toList()); + + return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.predicate)); + } +} 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 new file mode 100644 index 0000000000..d8cf794577 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.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 java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +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; + +/** + * Supporting class containing some state and convenience methods for building and executing fluent queries. + * + * @param The resulting type of the query. + * @author Greg Turnquist + * @since 2.6 + */ +abstract class FluentQuerySupport { + + protected final Class resultType; + protected final Sort sort; + protected final @Nullable Set properties; + protected final MappingContext, ? extends PersistentProperty> context; + + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + + FluentQuerySupport(Class resultType, Sort sort, @Nullable Collection properties, + MappingContext, ? extends PersistentProperty> context) { + + this.resultType = resultType; + this.sort = sort; + + if (properties != null) { + this.properties = new HashSet<>(properties); + } else { + this.properties = null; + } + + this.context = context; + } + + final Collection mergeProperties(Collection additionalProperties) { + + Set newProperties = new HashSet<>(); + if (this.properties != null) { + newProperties.addAll(this.properties); + } + newProperties.addAll(additionalProperties); + return Collections.unmodifiableCollection(newProperties); + } + + @SuppressWarnings("unchecked") + final Function getConversionFunction(Class inputType, Class targetType) { + + if (targetType.isAssignableFrom(inputType)) { + return (Function) Function.identity(); + } + + if (targetType.isInterface()) { + return o -> projectionFactory.createProjection(targetType, o); + } + + return o -> DefaultConversionService.getSharedInstance().convert(o, targetType); + } +} 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 14c893165a..32676777c7 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,8 +15,11 @@ */ package org.springframework.data.jpa.repository.support; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; import javax.persistence.EntityManager; import javax.persistence.LockModeType; @@ -25,10 +28,12 @@ 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; import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -51,6 +56,7 @@ * @author Jocelyn Ntakpe * @author Christoph Strobl * @author Jens Schauder + * @author Greg Turnquist */ public class QuerydslJpaPredicateExecutor implements QuerydslPredicateExecutor { @@ -80,9 +86,9 @@ public QuerydslJpaPredicateExecutor(JpaEntityInformation entityInformation } /* - * (non-Javadoc) - * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findOne(com.mysema.query.types.Predicate) - */ + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findOne(com.mysema.query.types.Predicate) + */ @Override public Optional findOne(Predicate predicate) { @@ -161,10 +167,45 @@ public Page findAll(Predicate predicate, Pageable pageable) { return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchCount); } + @Override + public R findBy(Predicate predicate, Function, R> queryFunction) { + + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(queryFunction, "Function must not be null!"); + + Function> finder = sort -> { + JPQLQuery select = createQuery(predicate).select(path); + + if (sort != null) { + select = querydsl.applySorting(sort, select); + } + + return select; + }; + + BiFunction> pagedFinder = (sort, pageable) -> { + + JPQLQuery select = finder.apply(sort); + + if (pageable.isPaged()) { + select = querydsl.applyPagination(pageable, select); + } + + return select; + }; + + FetchableFluentQuery fluentQuery = (FetchableFluentQuery) new FetchableFluentQueryByPredicate<>(predicate, + entityInformation.getJavaType(), finder, pagedFinder, this::count, this::exists, + this.entityInformation.getJavaType(), + new JpaMetamodelMappingContext(Collections.singleton(this.entityManager.getMetamodel()))); + + return queryFunction.apply(fluentQuery); + } + /* - * (non-Javadoc) - * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#count(com.mysema.query.types.Predicate) - */ + * (non-Javadoc) + * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#count(com.mysema.query.types.Predicate) + */ @Override public long count(Predicate predicate) { return createQuery(predicate).fetchCount(); diff --git a/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepository.java index cb92df3977..05307124fe 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepository.java @@ -18,6 +18,7 @@ import java.io.Serializable; import java.util.List; import java.util.Optional; +import java.util.function.Function; import javax.persistence.EntityManager; import javax.persistence.LockModeType; @@ -30,6 +31,7 @@ import org.springframework.data.querydsl.QSort; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -46,13 +48,14 @@ * QueryDsl specific extension of {@link SimpleJpaRepository} which adds implementation for * {@link QuerydslPredicateExecutor}. * - * @deprecated Instead of this class use {@link QuerydslJpaPredicateExecutor} * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch * @author Jocelyn Ntakpe * @author Christoph Strobl * @author Jens Schauder + * @author Greg Turnquist + * @deprecated Instead of this class use {@link QuerydslJpaPredicateExecutor} */ @Deprecated public class QuerydslJpaRepository extends SimpleJpaRepository @@ -164,6 +167,13 @@ public Page findAll(Predicate predicate, Pageable pageable) { return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchCount); } + @Override + public R findBy(Predicate predicate, + Function, R> queryFunction) { + throw new UnsupportedOperationException( + "Fluent Query API support for Querydsl is only found in QuerydslJpaPredicateExecutor."); + } + /* * (non-Javadoc) * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#count(com.mysema.query.types.Predicate) 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 2f3af6f25e..cc2b001a85 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 @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import javax.persistence.EntityManager; import javax.persistence.LockModeType; @@ -47,11 +48,16 @@ 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.repository.support.PageableExecutionUtils; import org.springframework.data.util.ProxyUtils; import org.springframework.data.util.Streamable; @@ -64,6 +70,8 @@ * Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer * you a more sophisticated interface than the plain {@link EntityManager} . * + * @param the type of the entity to handle + * @param the type of the entity's identifier * @author Oliver Gierke * @author Eberhard Wolff * @author Thomas Darimont @@ -75,8 +83,7 @@ * @author Moritz Becker * @author Sander Krabbenborg * @author Jesse Wouters - * @param the type of the entity to handle - * @param the type of the entity's identifier + * @author Greg Turnquist */ @Repository @Transactional(readOnly = true) @@ -87,6 +94,7 @@ 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; @@ -105,6 +113,9 @@ 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; } /** @@ -567,9 +578,30 @@ public Page findAll(Example example, Pageable pageable) { /* * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#count() + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findBy(org.springframework.data.domain.Example, java.util.function.Function) */ @Override + public R findBy(Example example, Function, R> queryFunction) { + + Function> finder = sort -> { + + ExampleSpecification spec = new ExampleSpecification<>(example, escapeCharacter); + Class probeType = example.getProbeType(); + + return getQuery(spec, probeType, sort); + }; + + FetchableFluentQuery fluentQuery = new FetchableFluentQueryByExample<>(example, finder, this::count, + this::exists, this.context, this.em, this.escapeCharacter); + + return queryFunction.apply(fluentQuery); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#count() + */ + @Override public long count() { return em.createQuery(getCountQueryString(), Long.class).getSingleResult(); } @@ -872,8 +904,8 @@ private static boolean isUnpaged(Pageable pageable) { * {@link SimpleJpaRepository#findAllById(Iterable)}. Workaround for OpenJPA not binding collections to in-clauses * correctly when using by-name binding. * - * @see OPENJPA-2018 * @author Oliver Gierke + * @see OPENJPA-2018 */ @SuppressWarnings("rawtypes") private static final class ByIdsSpecification implements Specification { @@ -905,9 +937,9 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild * {@link Specification} that gives access to the {@link Predicate} instance representing the values contained in the * {@link Example}. * + * @param * @author Christoph Strobl * @since 1.10 - * @param */ private static class ExampleSpecification implements Specification { 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 26ea26416e..29f03ef110 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -24,6 +24,8 @@ import static org.springframework.data.jpa.domain.Specification.not; import static org.springframework.data.jpa.domain.sample.UserSpecifications.*; +import lombok.Data; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -47,7 +49,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; @@ -75,7 +76,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.Transactional; - /** * Base integration test class for {@code UserRepository}. Loads a basic (non-namespace) Spring configuration file as * well as Hibernate configuration to execute tests. @@ -92,6 +92,7 @@ * @author Andrey Kovalev * @author Sander Krabbenborg * @author Jesse Wouters + * @author Greg Turnquist */ @ExtendWith(SpringExtension.class) @ContextConfiguration("classpath:application-context.xml") @@ -2030,6 +2031,186 @@ void findOneByExampleWithExcludedAttributes() { assertThat(repository.findOne(example)).contains(firstUser); } + @Test // GH-2294 + void findByFluentExampleWithSorting() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("v"); + + List users = repository.findBy( + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.sortBy(Sort.by("firstname")).all()); + + assertThat(users).containsExactly(thirdUser, firstUser, fourthUser); + } + + @Test // GH-2294 + void findByFluentExampleFirstValue() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("v"); + + User firstUser = repository.findBy( + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.sortBy(Sort.by("firstname")).firstValue()); + + assertThat(firstUser).isEqualTo(thirdUser); + } + + @Test // GH-2294 + void findByFluentExampleOneValue() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("v"); + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> { + repository.findBy( + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.sortBy(Sort.by("firstname")).oneValue()); + }); + } + + @Test // GH-2294 + void findByFluentExampleStream() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("v"); + + Stream userStream = repository.findBy( + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.sortBy(Sort.by("firstname")).stream()); + + assertThat(userStream).containsExactly(thirdUser, firstUser, fourthUser); + } + + @Test // GH-2294 + void findByFluentExamplePage() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("v"); + + Example userProbe = of(prototype, matching().withIgnorePaths("age", "createdAt", "active") + .withMatcher("firstname", GenericPropertyMatcher::contains)); + + Page page0 = repository.findBy(userProbe, // + q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2))); + + Page page1 = repository.findBy(userProbe, // + q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(1, 2))); + + assertThat(page0.getContent()).containsExactly(thirdUser, firstUser); + assertThat(page1.getContent()).containsExactly(fourthUser); + } + + @Test // GH-2294 + void findByFluentExampleWithInterfaceBasedProjection() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("v"); + + List users = repository.findBy( + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.as(UserProjectionInterfaceBased.class).all()); + + assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname) + .containsExactlyInAnyOrder(firstUser.getFirstname(), thirdUser.getFirstname(), fourthUser.getFirstname()); + } + + @Test // GH-2294 + void findByFluentExampleWithSortedInterfaceBasedProjection() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("v"); + + List users = repository.findBy( + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.as(UserProjectionInterfaceBased.class).sortBy(Sort.by("firstname")).all()); + + assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname) + .containsExactlyInAnyOrder(thirdUser.getFirstname(), firstUser.getFirstname(), fourthUser.getFirstname()); + } + + @Test // GH-2294 + void fluentExamplesWithClassBasedDtosNotYetSupported() { + + @Data + class UserDto { + String firstname; + } + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + + User prototype = new User(); + prototype.setFirstname("v"); + + repository.findBy( + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.as(UserDto.class).sortBy(Sort.by("firstname")).all()); + }); + } + + @Test // GH-2294 + void countByFluentExample() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("v"); + + long numOfUsers = repository.findBy( + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.sortBy(Sort.by("firstname")).count()); + + assertThat(numOfUsers).isEqualTo(3); + } + + @Test // GH-2294 + void existsByFluentExample() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("v"); + + boolean exists = repository.findBy( + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.sortBy(Sort.by("firstname")).exists()); + + assertThat(exists).isTrue(); + } + @Test // DATAJPA-218 void countByExampleWithExcludedAttributes() { @@ -2349,4 +2530,8 @@ private Page executeSpecWithSort(Sort sort) { assertThat(result.getTotalElements()).isEqualTo(2L); return result; } + + private interface UserProjectionInterfaceBased { + String getFirstname(); + } } 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 d707765826..49ee607867 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 @@ -17,17 +17,19 @@ import static org.assertj.core.api.Assertions.*; +import lombok.Data; + import java.sql.Date; +import java.time.LocalDate; import java.util.List; +import java.util.stream.Stream; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; -import java.time.LocalDate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -60,6 +62,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Malte Mauelshagen + * @author Greg Turnquist */ @ExtendWith(SpringExtension.class) @ContextConfiguration({ "classpath:infrastructure.xml" }) @@ -87,7 +90,8 @@ void setUp() { oliver = repository.save(new User("Oliver", "matthews", "oliver@matthews.com")); adminRole = em.merge(new Role("admin")); - this.predicateExecutor = new QuerydslJpaPredicateExecutor<>(information, em, SimpleEntityPathResolver.INSTANCE, null); + this.predicateExecutor = new QuerydslJpaPredicateExecutor<>(information, em, SimpleEntityPathResolver.INSTANCE, + null); } @Test @@ -217,7 +221,8 @@ void findBySpecificationWithSortByQueryDslOrderSpecifierWithQPageRequest() { QUser user = QUser.user; - Page page = predicateExecutor.findAll(user.firstname.isNotNull(), new QPageRequest(0, 10, user.firstname.asc())); + Page page = predicateExecutor.findAll(user.firstname.isNotNull(), + new QPageRequest(0, 10, user.firstname.asc())); assertThat(page.getContent()).containsExactly(carter, dave, oliver); } @@ -317,7 +322,127 @@ void findOneWithPredicateReturnsOptionalEmptyWhenNoDataFound() { @Test // DATAJPA-1115 void findOneWithPredicateThrowsExceptionForNonUniqueResults() { + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) .isThrownBy(() -> predicateExecutor.findOne(user.emailAddress.contains("com"))); } + + @Test // GH-2294 + void findByFluentPredicate() { + + List users = predicateExecutor.findBy(user.firstname.eq("Dave"), q -> q.sortBy(Sort.by("firstname")).all()); + + assertThat(users).containsExactly(dave); + } + + @Test // GH-2294 + void findByFluentPredicateWithSorting() { + + List users = predicateExecutor.findBy(user.firstname.isNotNull(), q -> q.sortBy(Sort.by("firstname")).all()); + + assertThat(users).containsExactly(carter, dave, oliver); + } + + @Test // GH-2294 + void findByFluentPredicateWithEqualsAndSorting() { + + List users = predicateExecutor.findBy(user.firstname.contains("v"), + q -> q.sortBy(Sort.by("firstname")).all()); + + assertThat(users).containsExactly(dave, oliver); + } + + @Test // GH-2294 + void findByFluentPredicateFirstValue() { + + User firstUser = predicateExecutor.findBy(user.firstname.contains("v"), + q -> q.sortBy(Sort.by("firstname")).firstValue()); + + assertThat(firstUser).isEqualTo(dave); + } + + @Test // GH-2294 + void findByFluentPredicateOneValue() { + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy( + () -> predicateExecutor.findBy(user.firstname.contains("v"), q -> q.sortBy(Sort.by("firstname")).oneValue())); + } + + @Test // GH-2294 + void findByFluentPredicateStream() { + + Stream userStream = predicateExecutor.findBy(user.firstname.contains("v"), + q -> q.sortBy(Sort.by("firstname")).stream()); + + assertThat(userStream).containsExactly(dave, oliver); + } + + @Test // GH-2294 + void findByFluentPredicatePage() { + + Predicate predicate = user.firstname.contains("v"); + + Page page0 = predicateExecutor.findBy(predicate, + q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 1))); + + Page page1 = predicateExecutor.findBy(predicate, + q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(1, 1))); + + assertThat(page0.getContent()).containsExactly(dave); + assertThat(page1.getContent()).containsExactly(oliver); + } + + @Test // GH-2294 + void findByFluentPredicateWithInterfaceBasedProjection() { + + List users = predicateExecutor.findBy(user.firstname.contains("v"), + q -> q.as(UserProjectionInterfaceBased.class).all()); + + assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname) + .containsExactlyInAnyOrder(dave.getFirstname(), oliver.getFirstname()); + } + + @Test // GH-2294 + void findByFluentPredicateWithSortedInterfaceBasedProjection() { + + 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()); + } + + @Test // GH-2294 + void countByFluentPredicate() { + + long userCount = predicateExecutor.findBy(user.firstname.contains("v"), + q -> q.sortBy(Sort.by("firstname")).count()); + + assertThat(userCount).isEqualTo(2); + } + + @Test // GH-2294 + void existsByFluentPredicate() { + + boolean exists = predicateExecutor.findBy(user.firstname.contains("v"), + q -> q.sortBy(Sort.by("firstname")).exists()); + + assertThat(exists).isTrue(); + } + + @Test // GH-2294 + void fluentExamplesWithClassBasedDtosNotYetSupported() { + + @Data + class UserDto { + String firstname; + } + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> predicateExecutor + .findBy(user.firstname.contains("v"), q -> q.as(UserDto.class).sortBy(Sort.by("firstname")).all())); + } + + private interface UserProjectionInterfaceBased { + String getFirstname(); + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepositoryTests.java index bff696d650..7d0df51936 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepositoryTests.java @@ -16,18 +16,21 @@ package org.springframework.data.jpa.repository.support; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.Example.*; +import static org.springframework.data.domain.ExampleMatcher.*; + +import lombok.Data; import java.sql.Date; +import java.time.LocalDate; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; -import java.time.LocalDate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -58,6 +61,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Malte Mauelshagen + * @author Greg Turnquist */ @ExtendWith(SpringExtension.class) @ContextConfiguration({ "classpath:infrastructure.xml" }) @@ -325,7 +329,15 @@ void findOneWithPredicateReturnsOptionalEmptyWhenNoDataFound() { @Test // DATAJPA-1115 void findOneWithPredicateThrowsExceptionForNonUniqueResults() { + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) .isThrownBy(() -> repository.findOne(user.emailAddress.contains("com"))); } + + @Test // GH-2294 + void findByFluentQuery() { + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> repository.findBy(user.firstname.eq("Dave"), q -> q.sortBy(Sort.by("firstname")).all())); + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java index 5a49cd8520..1c2d3827e9 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java @@ -35,7 +35,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.sample.User; @@ -186,12 +185,11 @@ void doNothingWhenNonExistentInstanceGetsDeleted() { newUser.setId(23); when(information.isNew(newUser)).thenReturn(false); - when(em.find(User.class,23)).thenReturn(null); + when(em.find(User.class, 23)).thenReturn(null); repo.delete(newUser); verify(em, never()).remove(newUser); verify(em, never()).merge(newUser); } - }