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