Implement FluentQuery for Querydsl and Query by Example.

Add support for both QueryByExampleExecutor and QuerydslPredicateExecutor. This is used in SimpleJpaRepository and QuerydslJpaPredicateExecutor, resulting in various test cases proving support by both examples and Querydsl predicates.

NOTE: Class-based DTOs are NOT supported yet.

See #2294
Original pull request: #2326.
This commit is contained in:
Greg L. Turnquist
2021-09-16 14:04:25 -05:00
committed by Mark Paluch
parent 58b73cf308
commit ea430f34aa
12 changed files with 875 additions and 28 deletions

View File

@@ -25,7 +25,7 @@
<hibernate>5.5.3.Final</hibernate>
<mysql-connector-java>8.0.23</mysql-connector-java>
<postgresql>42.2.19</postgresql>
<springdata.commons>2.6.0-SNAPSHOT</springdata.commons>
<springdata.commons>2.6.0-2228-SNAPSHOT</springdata.commons>
<vavr>0.10.3</vavr>
<hibernate.groupId>org.hibernate</hibernate.groupId>

View File

@@ -144,8 +144,8 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
/**
* 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<? extends Object> complete(Predicate predicate, Sort sort) {
@@ -271,10 +271,12 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
return getTypedPath(root, part).isNotNull();
case NOT_IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part)).in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression()).not();
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression()).not();
case IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part)).in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression());
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression());
case STARTING_WITH:
case ENDING_WITH:
case CONTAINING:

View File

@@ -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 <S> Domain type
* @param <R> Result type
* @author Greg Turnquist
* @since 2.6
*/
class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
private final Example<S> example;
private final Function<Sort, TypedQuery<S>> finder;
private final Function<Example<S>, Long> countOperation;
private final Function<Example<S>, Boolean> existsOperation;
private final EntityManager entityManager;
private final EscapeCharacter escapeCharacter;
public FetchableFluentQueryByExample(Example<S> example, Function<Sort, TypedQuery<S>> finder,
Function<Example<S>, Long> countOperation, Function<Example<S>, Boolean> existsOperation,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
EntityManager entityManager, EscapeCharacter escapeCharacter) {
this(example, (Class<R>) example.getProbeType(), Sort.unsorted(), null, finder, countOperation, existsOperation,
context, entityManager, escapeCharacter);
}
private FetchableFluentQueryByExample(Example<S> example, Class<R> returnType, Sort sort,
@Nullable Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
Function<Example<S>, Long> countOperation, Function<Example<S>, Boolean> existsOperation,
MappingContext<? extends PersistentEntity<?, ?>, ? 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<R> sortBy(Sort sort) {
return new FetchableFluentQueryByExample<S, R>(this.example, this.resultType, this.sort.and(sort), this.properties,
this.finder, this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
}
@Override
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
if (!resultType.isInterface()) {
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
}
return new FetchableFluentQueryByExample<S, NR>(this.example, resultType, this.sort, this.properties, this.finder,
this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
}
@Override
public FetchableFluentQuery<R> project(Collection<String> 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<S> limitedQuery = this.finder.apply(this.sort);
limitedQuery.setMaxResults(2); // Never need more than 2 values
List<R> 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<S> limitedQuery = this.finder.apply(this.sort);
limitedQuery.setMaxResults(1); // Never need more than 1 value
List<R> results = limitedQuery //
.getResultStream() //
.map(getConversionFunction(this.example.getProbeType(), this.resultType)) //
.collect(Collectors.toList());
return results.isEmpty() ? null : results.get(0);
}
@Override
public List<R> all() {
return stream().collect(Collectors.toList());
}
@Override
public Page<R> page(Pageable pageable) {
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
}
@Override
public Stream<R> 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<R> readPage(Pageable pageable) {
TypedQuery<S> pagedQuery = this.finder.apply(this.sort);
if (pageable.isPaged()) {
pagedQuery.setFirstResult((int) pageable.getOffset());
pagedQuery.setMaxResults(pageable.getPageSize());
}
List<R> paginatedResults = pagedQuery.getResultStream() //
.map(getConversionFunction(this.example.getProbeType(), this.resultType)) //
.collect(Collectors.toList());
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.example));
}
}

View File

@@ -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 <S> Domain type
* @param <R> Result type
* @author Greg Turnquist
* @since 2.6
*/
class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
private final Predicate predicate;
private final Function<Sort, JPQLQuery<S>> finder;
private final BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder;
private final Function<Predicate, Long> countOperation;
private final Function<Predicate, Boolean> existsOperation;
private final Class<S> entityType;
public FetchableFluentQueryByPredicate(Predicate predicate, Class<R> resultType, Function<Sort, JPQLQuery<S>> finder,
BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder, Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation, Class<S> entityType,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context) {
this(predicate, resultType, Sort.unsorted(), null, finder, pagedFinder, countOperation, existsOperation, entityType,
context);
}
private FetchableFluentQueryByPredicate(Predicate predicate, Class<R> resultType, Sort sort,
@Nullable Collection<String> properties, Function<Sort, JPQLQuery<S>> finder,
BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder, Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation, Class<S> entityType,
MappingContext<? extends PersistentEntity<?, ?>, ? 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<R> 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 <NR> FetchableFluentQuery<NR> as(Class<NR> 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<R> project(Collection<String> 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<R> 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<R> 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<R> all() {
return stream().collect(Collectors.toList());
}
@Override
public Page<R> page(Pageable pageable) {
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
}
@Override
public Stream<R> 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<R> readPage(Pageable pageable) {
JPQLQuery<S> pagedQuery = this.pagedFinder.apply(this.sort, pageable);
List<R> paginatedResults = pagedQuery.stream() //
.map(getConversionFunction(this.entityType, this.resultType)) //
.collect(Collectors.toList());
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.predicate));
}
}

View File

@@ -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 <R> The resulting type of the query.
* @author Greg Turnquist
* @since 2.6
*/
abstract class FluentQuerySupport<R> {
protected final Class<R> resultType;
protected final Sort sort;
protected final @Nullable Set<String> properties;
protected final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context;
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
FluentQuerySupport(Class<R> resultType, Sort sort, @Nullable Collection<String> properties,
MappingContext<? extends PersistentEntity<?, ?>, ? 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<String> mergeProperties(Collection<String> additionalProperties) {
Set<String> newProperties = new HashSet<>();
if (this.properties != null) {
newProperties.addAll(this.properties);
}
newProperties.addAll(additionalProperties);
return Collections.unmodifiableCollection(newProperties);
}
@SuppressWarnings("unchecked")
final <S> Function<Object, R> getConversionFunction(Class<S> inputType, Class<R> targetType) {
if (targetType.isAssignableFrom(inputType)) {
return (Function<Object, R>) Function.identity();
}
if (targetType.isInterface()) {
return o -> projectionFactory.createProjection(targetType, o);
}
return o -> DefaultConversionService.getSharedInstance().convert(o, targetType);
}
}

View File

@@ -15,8 +15,11 @@
*/
package org.springframework.data.jpa.repository.support;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
@@ -25,10 +28,12 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QSort;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -51,6 +56,7 @@ import com.querydsl.jpa.impl.AbstractJPAQuery;
* @author Jocelyn Ntakpe
* @author Christoph Strobl
* @author Jens Schauder
* @author Greg Turnquist
*/
public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecutor<T> {
@@ -80,9 +86,9 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
}
/*
* (non-Javadoc)
* @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findOne(com.mysema.query.types.Predicate)
*/
* (non-Javadoc)
* @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findOne(com.mysema.query.types.Predicate)
*/
@Override
public Optional<T> findOne(Predicate predicate) {
@@ -161,10 +167,45 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchCount);
}
@Override
public <S extends T, R> R findBy(Predicate predicate, Function<FetchableFluentQuery<S>, R> queryFunction) {
Assert.notNull(predicate, "Predicate must not be null!");
Assert.notNull(queryFunction, "Function must not be null!");
Function<Sort, JPQLQuery<T>> finder = sort -> {
JPQLQuery<T> select = createQuery(predicate).select(path);
if (sort != null) {
select = querydsl.applySorting(sort, select);
}
return select;
};
BiFunction<Sort, Pageable, JPQLQuery<T>> pagedFinder = (sort, pageable) -> {
JPQLQuery<T> select = finder.apply(sort);
if (pageable.isPaged()) {
select = querydsl.applyPagination(pageable, select);
}
return select;
};
FetchableFluentQuery<S> fluentQuery = (FetchableFluentQuery<S>) 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();

View File

@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.support;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
@@ -30,6 +31,7 @@ import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QSort;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -46,13 +48,14 @@ import com.querydsl.jpa.impl.AbstractJPAQuery;
* QueryDsl specific extension of {@link SimpleJpaRepository} which adds implementation for
* {@link QuerydslPredicateExecutor}.
*
* @deprecated Instead of this class use {@link QuerydslJpaPredicateExecutor}
* @author Oliver Gierke
* @author Thomas Darimont
* @author Mark Paluch
* @author Jocelyn Ntakpe
* @author Christoph Strobl
* @author Jens Schauder
* @author Greg Turnquist
* @deprecated Instead of this class use {@link QuerydslJpaPredicateExecutor}
*/
@Deprecated
public class QuerydslJpaRepository<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
@@ -164,6 +167,13 @@ public class QuerydslJpaRepository<T, ID extends Serializable> extends SimpleJpa
return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchCount);
}
@Override
public <S extends T, R> R findBy(Predicate predicate,
Function<FluentQuery.FetchableFluentQuery<S>, 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)

View File

@@ -24,6 +24,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
@@ -47,11 +48,16 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.data.jpa.repository.support.QueryHints.NoHints;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.data.util.ProxyUtils;
import org.springframework.data.util.Streamable;
@@ -64,6 +70,8 @@ import org.springframework.util.Assert;
* Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer
* you a more sophisticated interface than the plain {@link EntityManager} .
*
* @param <T> the type of the entity to handle
* @param <ID> the type of the entity's identifier
* @author Oliver Gierke
* @author Eberhard Wolff
* @author Thomas Darimont
@@ -75,8 +83,7 @@ import org.springframework.util.Assert;
* @author Moritz Becker
* @author Sander Krabbenborg
* @author Jesse Wouters
* @param <T> the type of the entity to handle
* @param <ID> the type of the entity's identifier
* @author Greg Turnquist
*/
@Repository
@Transactional(readOnly = true)
@@ -87,6 +94,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
private final JpaEntityInformation<T, ?> entityInformation;
private final EntityManager em;
private final PersistenceProvider provider;
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context;
private @Nullable CrudMethodMetadata metadata;
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
@@ -105,6 +113,9 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
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 class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
/*
* (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 <S extends T, R> R findBy(Example<S> example, Function<FetchableFluentQuery<S>, R> queryFunction) {
Function<Sort, TypedQuery<S>> finder = sort -> {
ExampleSpecification<S> spec = new ExampleSpecification<>(example, escapeCharacter);
Class<S> probeType = example.getProbeType();
return getQuery(spec, probeType, sort);
};
FetchableFluentQuery<S> fluentQuery = new FetchableFluentQueryByExample<>(example, finder, this::count,
this::exists, this.context, this.em, this.escapeCharacter);
return queryFunction.apply(fluentQuery);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#count()
*/
@Override
public long count() {
return em.createQuery(getCountQueryString(), Long.class).getSingleResult();
}
@@ -872,8 +904,8 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
* {@link SimpleJpaRepository#findAllById(Iterable)}. Workaround for OpenJPA not binding collections to in-clauses
* correctly when using by-name binding.
*
* @see <a href="https://issues.apache.org/jira/browse/OPENJPA-2018?focusedCommentId=13924055">OPENJPA-2018</a>
* @author Oliver Gierke
* @see <a href="https://issues.apache.org/jira/browse/OPENJPA-2018?focusedCommentId=13924055">OPENJPA-2018</a>
*/
@SuppressWarnings("rawtypes")
private static final class ByIdsSpecification<T> implements Specification<T> {
@@ -905,9 +937,9 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
* {@link Specification} that gives access to the {@link Predicate} instance representing the values contained in the
* {@link Example}.
*
* @param <T>
* @author Christoph Strobl
* @since 1.10
* @param <T>
*/
private static class ExampleSpecification<T> implements Specification<T> {

View File

@@ -24,6 +24,8 @@ import static org.springframework.data.jpa.domain.Specification.*;
import static org.springframework.data.jpa.domain.Specification.not;
import static org.springframework.data.jpa.domain.sample.UserSpecifications.*;
import lombok.Data;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -47,7 +49,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
@@ -75,7 +76,6 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;
/**
* Base integration test class for {@code UserRepository}. Loads a basic (non-namespace) Spring configuration file as
* well as Hibernate configuration to execute tests.
@@ -92,6 +92,7 @@ import org.springframework.transaction.annotation.Transactional;
* @author Andrey Kovalev
* @author Sander Krabbenborg
* @author Jesse Wouters
* @author Greg Turnquist
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:application-context.xml")
@@ -2030,6 +2031,186 @@ public class UserRepositoryTests {
assertThat(repository.findOne(example)).contains(firstUser);
}
@Test // GH-2294
void findByFluentExampleWithSorting() {
flushTestUsers();
User prototype = new User();
prototype.setFirstname("v");
List<User> 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<User> 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<User> userProbe = of(prototype, matching().withIgnorePaths("age", "createdAt", "active")
.withMatcher("firstname", GenericPropertyMatcher::contains));
Page<User> page0 = repository.findBy(userProbe, //
q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2)));
Page<User> 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<UserProjectionInterfaceBased> 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<UserProjectionInterfaceBased> users = repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.as(UserProjectionInterfaceBased.class).sortBy(Sort.by("firstname")).all());
assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname)
.containsExactlyInAnyOrder(thirdUser.getFirstname(), firstUser.getFirstname(), fourthUser.getFirstname());
}
@Test // GH-2294
void fluentExamplesWithClassBasedDtosNotYetSupported() {
@Data
class UserDto {
String firstname;
}
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> {
User prototype = new User();
prototype.setFirstname("v");
repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.as(UserDto.class).sortBy(Sort.by("firstname")).all());
});
}
@Test // GH-2294
void countByFluentExample() {
flushTestUsers();
User prototype = new User();
prototype.setFirstname("v");
long numOfUsers = repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.sortBy(Sort.by("firstname")).count());
assertThat(numOfUsers).isEqualTo(3);
}
@Test // GH-2294
void existsByFluentExample() {
flushTestUsers();
User prototype = new User();
prototype.setFirstname("v");
boolean exists = repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.sortBy(Sort.by("firstname")).exists());
assertThat(exists).isTrue();
}
@Test // DATAJPA-218
void countByExampleWithExcludedAttributes() {
@@ -2349,4 +2530,8 @@ public class UserRepositoryTests {
assertThat(result.getTotalElements()).isEqualTo(2L);
return result;
}
private interface UserProjectionInterfaceBased {
String getFirstname();
}
}

View File

@@ -17,17 +17,19 @@ package org.springframework.data.jpa.repository.support;
import static org.assertj.core.api.Assertions.*;
import lombok.Data;
import java.sql.Date;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Stream;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -60,6 +62,7 @@ import com.querydsl.core.types.dsl.PathBuilderFactory;
* @author Mark Paluch
* @author Christoph Strobl
* @author Malte Mauelshagen
* @author Greg Turnquist
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration({ "classpath:infrastructure.xml" })
@@ -87,7 +90,8 @@ class QuerydslJpaPredicateExecutorUnitTests {
oliver = repository.save(new User("Oliver", "matthews", "oliver@matthews.com"));
adminRole = em.merge(new Role("admin"));
this.predicateExecutor = new QuerydslJpaPredicateExecutor<>(information, em, SimpleEntityPathResolver.INSTANCE, null);
this.predicateExecutor = new QuerydslJpaPredicateExecutor<>(information, em, SimpleEntityPathResolver.INSTANCE,
null);
}
@Test
@@ -217,7 +221,8 @@ class QuerydslJpaPredicateExecutorUnitTests {
QUser user = QUser.user;
Page<User> page = predicateExecutor.findAll(user.firstname.isNotNull(), new QPageRequest(0, 10, user.firstname.asc()));
Page<User> page = predicateExecutor.findAll(user.firstname.isNotNull(),
new QPageRequest(0, 10, user.firstname.asc()));
assertThat(page.getContent()).containsExactly(carter, dave, oliver);
}
@@ -317,7 +322,127 @@ class QuerydslJpaPredicateExecutorUnitTests {
@Test // DATAJPA-1115
void findOneWithPredicateThrowsExceptionForNonUniqueResults() {
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class)
.isThrownBy(() -> predicateExecutor.findOne(user.emailAddress.contains("com")));
}
@Test // GH-2294
void findByFluentPredicate() {
List<User> users = predicateExecutor.findBy(user.firstname.eq("Dave"), q -> q.sortBy(Sort.by("firstname")).all());
assertThat(users).containsExactly(dave);
}
@Test // GH-2294
void findByFluentPredicateWithSorting() {
List<User> 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<User> 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<User> 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<User> page0 = predicateExecutor.findBy(predicate,
q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 1)));
Page<User> 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<UserProjectionInterfaceBased> 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<UserProjectionInterfaceBased> 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();
}
}

View File

@@ -16,18 +16,21 @@
package org.springframework.data.jpa.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.domain.Example.*;
import static org.springframework.data.domain.ExampleMatcher.*;
import lombok.Data;
import java.sql.Date;
import java.time.LocalDate;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -58,6 +61,7 @@ import com.querydsl.core.types.dsl.PathBuilderFactory;
* @author Mark Paluch
* @author Christoph Strobl
* @author Malte Mauelshagen
* @author Greg Turnquist
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration({ "classpath:infrastructure.xml" })
@@ -325,7 +329,15 @@ class QuerydslJpaRepositoryTests {
@Test // DATAJPA-1115
void findOneWithPredicateThrowsExceptionForNonUniqueResults() {
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class)
.isThrownBy(() -> repository.findOne(user.emailAddress.contains("com")));
}
@Test // GH-2294
void findByFluentQuery() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> repository.findBy(user.firstname.eq("Dave"), q -> q.sortBy(Sort.by("firstname")).all()));
}
}

View File

@@ -35,7 +35,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.sample.User;
@@ -186,12 +185,11 @@ class SimpleJpaRepositoryUnitTests {
newUser.setId(23);
when(information.isNew(newUser)).thenReturn(false);
when(em.find(User.class,23)).thenReturn(null);
when(em.find(User.class, 23)).thenReturn(null);
repo.delete(newUser);
verify(em, never()).remove(newUser);
verify(em, never()).merge(newUser);
}
}