From 11e45589e6e588c0e169ffc0ff3ed40d7a9160d1 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 manifests in SimpleJpaRepository and QuerydslJpaPredicateExecutor, resulting in various test cases proving support by both examples and Querydsl predicates. Closes #2294. --- pom.xml | 2 +- .../query/DtoInstantiatingConverter.java | 73 ++++++ .../query/FetchableFluentQueryByExample.java | 208 ++++++++++++++++++ .../FetchableFluentQueryByPredicate.java | 165 ++++++++++++++ .../repository/query/FluentQuerySupport.java | 87 ++++++++ .../jpa/repository/query/JpaQueryCreator.java | 10 +- .../support/QuerydslJpaPredicateExecutor.java | 57 ++++- .../support/QuerydslJpaRepository.java | 12 +- .../support/SimpleJpaRepository.java | 45 +++- .../jpa/repository/UserRepositoryTests.java | 166 +++++++++++++- ...QuerydslJpaPredicateExecutorUnitTests.java | 119 +++++++++- .../support/QuerydslJpaRepositoryTests.java | 11 +- .../support/SimpleJpaRepositoryUnitTests.java | 4 +- 13 files changed, 930 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/DtoInstantiatingConverter.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/FetchableFluentQueryByExample.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/FetchableFluentQueryByPredicate.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/FluentQuerySupport.java diff --git a/pom.xml b/pom.xml index dd472ffbe2b..ec572e66ae7 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/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/jpa/repository/query/DtoInstantiatingConverter.java new file mode 100644 index 00000000000..b691bb54cd6 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/DtoInstantiatingConverter.java @@ -0,0 +1,73 @@ +package org.springframework.data.jpa.repository.query; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.util.Assert; + +public class DtoInstantiatingConverter implements Converter { + + private final Class targetType; + private final MappingContext, ? extends PersistentProperty> context; + private final EntityInstantiator instantiator; + + public DtoInstantiatingConverter(Class dtoType, + MappingContext, ? extends PersistentProperty> context, + EntityInstantiators entityInstantiators) { + + Assert.notNull(dtoType, "DTO type must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(entityInstantiators, "EntityInstantiators must not be null!"); + + this.targetType = dtoType; + this.context = context; + System.out.println(this.context.getManagedTypes()); + PersistentEntity> requiredPersistentEntity = context + .getRequiredPersistentEntity(dtoType); + this.instantiator = entityInstantiators.getInstantiatorFor(requiredPersistentEntity); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Object convert(Object source) { + + if (targetType.isInterface()) { + return source; + } + + PersistentEntity sourceEntity = context.getRequiredPersistentEntity(source.getClass()); + PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); + PersistentEntity targetEntity = context.getRequiredPersistentEntity(targetType); + PreferredConstructor> constructor = targetEntity.getPersistenceConstructor(); + + Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { + + @Override + public Object getParameterValue(Parameter parameter) { + return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName())); + } + }); + + PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); + + targetEntity.doWithProperties((SimplePropertyHandler) property -> { + + if (constructor.isConstructorParameter(property)) { + return; + } + + dtoAccessor.setProperty(property, + sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName()))); + }); + + return dto; + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/FetchableFluentQueryByExample.java b/src/main/java/org/springframework/data/jpa/repository/query/FetchableFluentQueryByExample.java new file mode 100644 index 00000000000..1ab39ab7d87 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/FetchableFluentQueryByExample.java @@ -0,0 +1,208 @@ +/* + * Copyright 2013-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.query; + +import static org.springframework.data.jpa.repository.query.QueryUtils.*; + +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 javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +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.convert.QueryByExamplePredicateBuilder; +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 + * @author Michael J. Simons + * @author Mark Paluch + * @since 2.6 + */ +public class FetchableFluentQueryByExample extends FluentQuerySupport implements FetchableFluentQuery { + + private Example example; + private Function> finder; + private Function, Long> countOperation; + private Function, Boolean> existsOperation; + private EntityManager entityManager; + private 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) { + + 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() { + + List all = all(); + + if (all.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + + return all.isEmpty() ? null : all.get(0); + } + + @Override + public R firstValue() { + + List all = all(); + return all.isEmpty() ? null : all.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() { + + if (this.resultType != this.example.getProbeType()) { + System.out.println("You have a projection!"); + } + + 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)); + } + + /** + * Draft version of a projection-based query using class-based DTOs. + * + * @param sort + * @param queryType + * @param example + * @return + */ + private TypedQuery createProjectionQueryByExample(Sort sort, Class queryType, Example example) { + + CriteriaBuilder builder = this.entityManager.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(queryType); + + Root root = query.from(queryType); + query.select(root); + + Predicate predicate = QueryByExamplePredicateBuilder.getPredicate( + builder.createQuery(example.getProbeType()).from(example.getProbeType()), builder, example, escapeCharacter); + + if (predicate != null) { + query.where(predicate); + } + + if (sort.isSorted()) { + query.orderBy(toOrders(sort, root, builder)); + } + + return this.entityManager.createQuery(query); + } + +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/FetchableFluentQueryByPredicate.java b/src/main/java/org/springframework/data/jpa/repository/query/FetchableFluentQueryByPredicate.java new file mode 100644 index 00000000000..a960297183a --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/FetchableFluentQueryByPredicate.java @@ -0,0 +1,165 @@ +/* + * Copyright 2013-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.query; + +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 + * @author Michael J. Simons + * @since 2.6 + */ +public class FetchableFluentQueryByPredicate extends FluentQuerySupport implements FetchableFluentQuery { + + private Predicate predicate; + private Function> finder; + private BiFunction> pagedFinder; + private Function countOperation; + private Function existsOperation; + private 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) { + + 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 all = all(); + + if (all.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + + return all.isEmpty() ? null : all.get(0); + } + + @Override + public R firstValue() { + + List all = all(); + return all.isEmpty() ? null : all.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() { + + if (this.resultType != this.entityType) { + System.out.println("You have a projection!"); + } + + 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/query/FluentQuerySupport.java b/src/main/java/org/springframework/data/jpa/repository/query/FluentQuerySupport.java new file mode 100644 index 00000000000..3299a0c5a15 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/FluentQuerySupport.java @@ -0,0 +1,87 @@ +/* + * Copyright 2013-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.query; + +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 + * @author Michael J. Simons + * @since 2.6 + */ +abstract class FluentQuerySupport { + + protected Class resultType; + protected Sort sort; + @Nullable protected Set properties; + protected 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/query/JpaQueryCreator.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 6c9407852ef..825d8a3415d 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/QuerydslJpaPredicateExecutor.java b/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index 14c893165a6..8c06b3463fe 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,13 @@ 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.jpa.repository.query.FetchableFluentQueryByPredicate; 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 +57,7 @@ * @author Jocelyn Ntakpe * @author Christoph Strobl * @author Jens Schauder + * @author Greg Turnquist */ public class QuerydslJpaPredicateExecutor implements QuerydslPredicateExecutor { @@ -80,9 +87,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 +168,48 @@ 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; + }; + + Function countOperation = p -> count(predicate); + Function existsOperation = p -> exists(predicate); + + FetchableFluentQuery fluentQuery = (FetchableFluentQuery) new FetchableFluentQueryByPredicate<>(predicate, + entityInformation.getJavaType(), finder, pagedFinder, countOperation, existsOperation, + 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 cb92df39776..05307124feb 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 2f3af6f25ec..d31fae5b00e 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,17 @@ 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.FetchableFluentQueryByExample; 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 +71,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 +84,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 +95,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 +114,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 +579,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 +905,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,11 +938,11 @@ 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 { + public static class ExampleSpecification implements Specification { private static final long serialVersionUID = 1L; 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 26ea26416ea..ce36c9e2823 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -47,7 +47,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 +74,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 +90,7 @@ * @author Andrey Kovalev * @author Sander Krabbenborg * @author Jesse Wouters + * @author Greg Turnquist */ @ExtendWith(SpringExtension.class) @ContextConfiguration("classpath:application-context.xml") @@ -2030,6 +2029,165 @@ 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 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 +2507,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 d707765826a..7065a3ed2b1 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 @@ -18,16 +18,16 @@ import static org.assertj.core.api.Assertions.*; 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 +60,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Malte Mauelshagen + * @author Greg Turnquist */ @ExtendWith(SpringExtension.class) @ContextConfiguration({ "classpath:infrastructure.xml" }) @@ -87,7 +88,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 +219,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 +320,115 @@ 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(); + } + + 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 bff696d650e..3b46e6663f9 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 @@ -18,16 +18,15 @@ import static org.assertj.core.api.Assertions.*; 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 +57,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Malte Mauelshagen + * @author Greg Turnquist */ @ExtendWith(SpringExtension.class) @ContextConfiguration({ "classpath:infrastructure.xml" }) @@ -328,4 +328,11 @@ 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 5a49cd85207..1c2d3827e93 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); } - }