From ea430f34aa35a6a188a927148eed6abcb501c991 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. See #2294 Original pull request: #2326. --- 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 e31849e91..d91c077f2 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 6c9407852..825d8a341 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 @@ public class JpaQueryCreator extends AbstractQueryCreator complete(Predicate predicate, Sort sort) { @@ -271,10 +271,12 @@ public class JpaQueryCreator extends AbstractQueryCreator>) 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 000000000..798f4663e --- /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 000000000..ba2b0790f --- /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 000000000..d8cf79457 --- /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 14c893165..32676777c 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.dao.IncorrectResultSizeDataAccessException; 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 @@ import com.querydsl.jpa.impl.AbstractJPAQuery; * @author Jocelyn Ntakpe * @author Christoph Strobl * @author Jens Schauder + * @author Greg Turnquist */ public class QuerydslJpaPredicateExecutor implements QuerydslPredicateExecutor { @@ -80,9 +86,9 @@ public class QuerydslJpaPredicateExecutor implements QuerydslPredicateExecuto } /* - * (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 class QuerydslJpaPredicateExecutor implements QuerydslPredicateExecuto 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 cb92df397..05307124f 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 @@ package org.springframework.data.jpa.repository.support; 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.EntityPathResolver; 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 @@ import com.querydsl.jpa.impl.AbstractJPAQuery; * 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 class QuerydslJpaRepository extends SimpleJpa 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 2f3af6f25..cc2b001a8 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.HashMap; 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.Pageable; 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 @@ import org.springframework.util.Assert; * 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 @@ import org.springframework.util.Assert; * @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 class SimpleJpaRepository implements JpaRepositoryImplementation implements JpaRepositoryImplementation 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 @@ public class SimpleJpaRepository implements JpaRepositoryImplementationOPENJPA-2018 * @author Oliver Gierke + * @see OPENJPA-2018 */ @SuppressWarnings("rawtypes") private static final class ByIdsSpecification implements Specification { @@ -905,9 +937,9 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation * @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 26ea26416..29f03ef11 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.*; 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.BeforeEach; 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.ContextConfiguration; 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 @@ import org.springframework.transaction.annotation.Transactional; * @author Andrey Kovalev * @author Sander Krabbenborg * @author Jesse Wouters + * @author Greg Turnquist */ @ExtendWith(SpringExtension.class) @ContextConfiguration("classpath:application-context.xml") @@ -2030,6 +2031,186 @@ public class UserRepositoryTests { 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 @@ public class UserRepositoryTests { 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 d70776582..49ee60786 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 @@ package org.springframework.data.jpa.repository.support; 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 @@ import com.querydsl.core.types.dsl.PathBuilderFactory; * @author Mark Paluch * @author Christoph Strobl * @author Malte Mauelshagen + * @author Greg Turnquist */ @ExtendWith(SpringExtension.class) @ContextConfiguration({ "classpath:infrastructure.xml" }) @@ -87,7 +90,8 @@ class QuerydslJpaPredicateExecutorUnitTests { 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 @@ class QuerydslJpaPredicateExecutorUnitTests { 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 @@ class QuerydslJpaPredicateExecutorUnitTests { @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 bff696d65..7d0df5193 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 @@ import com.querydsl.core.types.dsl.PathBuilderFactory; * @author Mark Paluch * @author Christoph Strobl * @author Malte Mauelshagen + * @author Greg Turnquist */ @ExtendWith(SpringExtension.class) @ContextConfiguration({ "classpath:infrastructure.xml" }) @@ -325,7 +329,15 @@ class QuerydslJpaRepositoryTests { @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 5a49cd852..1c2d3827e 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.Mock; 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 @@ class SimpleJpaRepositoryUnitTests { 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); } - }