diff --git a/pom.xml b/pom.xml index 5d28c8a5c5..816131dcf6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-3757-SNAPSHOT pom Spring Data MongoDB @@ -26,7 +26,7 @@ multi spring-data-mongodb - 2.6.0-SNAPSHOT + 2.6.0-2228-SNAPSHOT 4.3.1 ${mongo} 1.19 diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 0033bd11d5..b3e528fe1d 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-3757-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index f62c8dc7f4..45506edc05 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-3757-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 1f157e75bc..645ffd36b5 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-3757-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index fb0780c5c8..11a75828a2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -1385,7 +1385,6 @@ public T save(T objectToSave, String collectionName) { return source.isVersionedEntity() // ? doSaveVersioned(source, collectionName) // : (T) doSave(collectionName, objectToSave, this.mongoConverter); - } @SuppressWarnings("unchecked") diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java index 4de789e12f..cc2ae28c23 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java @@ -164,9 +164,9 @@ public boolean isSorted() { * @throws IllegalArgumentException when {@code fieldsObject} is {@literal null}. * @since 1.6 */ - protected void setFieldsObject(Document fieldsObject) { + public void setFieldsObject(Document fieldsObject) { - Assert.notNull(sortObject, "Field document must not be null"); + Assert.notNull(fieldsObject, "Field document must not be null"); this.fieldsObject = fieldsObject; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableFluentQuerySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableFluentQuerySupport.java new file mode 100644 index 0000000000..d910106bd0 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableFluentQuerySupport.java @@ -0,0 +1,100 @@ +/* + * 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.mongodb.repository.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.util.Assert; + +/** + * Support class for {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} implementations. + * + * @author Mark Paluch + * @since 3.3 + */ +abstract class FetchableFluentQuerySupport implements FluentQuery.FetchableFluentQuery { + + private final P predicate; + private final Sort sort; + private final Class resultType; + private final List fieldsToInclude; + + FetchableFluentQuerySupport(P predicate, Sort sort, Class resultType, List fieldsToInclude) { + this.predicate = predicate; + this.sort = sort; + this.resultType = resultType; + this.fieldsToInclude = fieldsToInclude; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#sortBy(org.springframework.data.domain.Sort) + */ + @Override + public FluentQuery.FetchableFluentQuery sortBy(Sort sort) { + + Assert.notNull(sort, "Sort must not be null!"); + + return create(predicate, sort, resultType, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class) + */ + @Override + public FluentQuery.FetchableFluentQuery as(Class projection) { + + Assert.notNull(projection, "Projection target type must not be null!"); + + return create(predicate, sort, projection, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection) + */ + @Override + public FluentQuery.FetchableFluentQuery project(Collection properties) { + + Assert.notNull(properties, "Projection properties must not be null!"); + + return create(predicate, sort, resultType, new ArrayList<>(properties)); + } + + protected abstract FetchableFluentQuerySupport create(P predicate, Sort sort, Class resultType, + List fieldsToInclude); + + P getPredicate() { + return predicate; + } + + Sort getSort() { + return sort; + } + + Class getResultType() { + return resultType; + } + + List getFieldsToInclude() { + return fieldsToInclude; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java index 569273afb5..5255dedaa4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java @@ -15,18 +15,24 @@ */ package org.springframework.data.mongodb.repository.support; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; +import org.bson.Document; 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.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.util.Assert; @@ -184,6 +190,21 @@ public boolean exists(Predicate predicate) { return createQueryFor(predicate).fetchCount() > 0; } + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findBy(com.querydsl.core.types.Predicate, java.util.function.Function) + */ + @Override + @SuppressWarnings("unchecked") + public R findBy(Predicate predicate, + Function, R> queryFunction) { + + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(queryFunction, "Query function must not be null!"); + + return queryFunction.apply(new FluentQuerydsl<>(predicate, (Class) typeInformation().getJavaType())); + } + /** * Creates a {@link SpringDataMongodbQuery} for the given {@link Predicate}. * @@ -232,4 +253,113 @@ private SpringDataMongodbQuery applySorting(SpringDataMongodbQuery query, toOrderSpecifiers(sort).forEach(query::orderBy); return query; } + + /** + * {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using Querydsl + * {@link Predicate}. + * + * @author Mark Paluch + * @since 3.3 + */ + class FluentQuerydsl extends FetchableFluentQuerySupport { + + FluentQuerydsl(Predicate predicate, Class resultType) { + this(predicate, Sort.unsorted(), resultType, Collections.emptyList()); + } + + FluentQuerydsl(Predicate predicate, Sort sort, Class resultType, List fieldsToInclude) { + super(predicate, sort, resultType, fieldsToInclude); + } + + @Override + protected FluentQuerydsl create(Predicate predicate, Sort sort, Class resultType, + List fieldsToInclude) { + return new FluentQuerydsl<>(predicate, sort, resultType, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() + */ + @Override + public T oneValue() { + return createQuery().fetchOne(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() + */ + @Override + public T firstValue() { + return createQuery().fetchFirst(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() + */ + @Override + public List all() { + return createQuery().fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) + */ + @Override + public Page page(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null!"); + + return createQuery().fetchPage(pageable); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() + */ + @Override + public Stream stream() { + return createQuery().stream(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() + */ + @Override + public long count() { + return createQuery().fetchCount(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() + */ + @Override + public boolean exists() { + return count() > 0; + } + + private SpringDataMongodbQuery createQuery() { + return new SpringDataMongodbQuery<>(mongoOperations, typeInformation().getJavaType(), getResultType(), + mongoOperations.getCollectionName(typeInformation().getJavaType()), this::customize).where(getPredicate()); + } + + private void customize(BasicQuery query) { + + List fieldsToInclude = getFieldsToInclude(); + if (!fieldsToInclude.isEmpty()) { + Document fields = new Document(); + fieldsToInclude.forEach(field -> fields.put(field, 1)); + query.setFieldsObject(fields); + } + + if (getSort().isSorted()) { + query.with(getSort()); + } + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveFluentQuerySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveFluentQuerySupport.java new file mode 100644 index 0000000000..ee5ef32555 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveFluentQuerySupport.java @@ -0,0 +1,100 @@ +/* + * 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.mongodb.repository.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.util.Assert; + +/** + * Support class for {@link org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery} implementations. + * + * @author Mark Paluch + * @since 3.3 + */ +abstract class ReactiveFluentQuerySupport implements FluentQuery.ReactiveFluentQuery { + + private final P predicate; + private final Sort sort; + private final Class resultType; + private final List fieldsToInclude; + + ReactiveFluentQuerySupport(P predicate, Sort sort, Class resultType, List fieldsToInclude) { + this.predicate = predicate; + this.sort = sort; + this.resultType = resultType; + this.fieldsToInclude = fieldsToInclude; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#sortBy(org.springframework.data.domain.Sort) + */ + @Override + public ReactiveFluentQuery sortBy(Sort sort) { + + Assert.notNull(sort, "Sort must not be null!"); + + return create(predicate, sort, resultType, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#as(java.lang.Class) + */ + @Override + public ReactiveFluentQuery as(Class projection) { + + Assert.notNull(projection, "Projection target type must not be null!"); + + return create(predicate, sort, projection, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#project(java.util.Collection) + */ + @Override + public ReactiveFluentQuery project(Collection properties) { + + Assert.notNull(properties, "Projection properties must not be null!"); + + return create(predicate, sort, resultType, new ArrayList<>(properties)); + } + + protected abstract ReactiveFluentQuerySupport create(P predicate, Sort sort, Class resultType, + List fieldsToInclude); + + P getPredicate() { + return predicate; + } + + Sort getSort() { + return sort; + } + + Class getResultType() { + return resultType; + } + + List getFieldsToInclude() { + return fieldsToInclude; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactivePageableExecutionUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactivePageableExecutionUtils.java new file mode 100644 index 0000000000..2bcbdba6ae --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactivePageableExecutionUtils.java @@ -0,0 +1,69 @@ +/* + * 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.mongodb.repository.support; + +import reactor.core.publisher.Mono; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.util.Assert; + +/** + * Support for query execution using {@link Pageable}. Using {@link ReactivePageableExecutionUtils} assumes that data + * queries are cheaper than {@code COUNT} queries and so some cases can take advantage of optimizations. + * + * @author Mark Paluch + * @since 3.3 + */ +abstract class ReactivePageableExecutionUtils { + + private ReactivePageableExecutionUtils() {} + + /** + * Constructs a {@link Page} based on the given {@code content}, {@link Pageable} and {@link Mono} applying + * optimizations. The construction of {@link Page} omits a count query if the total can be determined based on the + * result size and {@link Pageable}. + * + * @param content must not be {@literal null}. + * @param pageable must not be {@literal null}. + * @param totalSupplier must not be {@literal null}. + * @return the {@link Page}. + */ + public static Mono> getPage(List content, Pageable pageable, Mono totalSupplier) { + + Assert.notNull(content, "Content must not be null!"); + Assert.notNull(pageable, "Pageable must not be null!"); + Assert.notNull(totalSupplier, "TotalSupplier must not be null!"); + + if (pageable.isUnpaged() || pageable.getOffset() == 0) { + + if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) { + return Mono.just(new PageImpl<>(content, pageable, content.size())); + } + + return totalSupplier.map(total -> new PageImpl<>(content, pageable, total)); + } + + if (content.size() != 0 && pageable.getPageSize() > content.size()) { + return Mono.just(new PageImpl<>(content, pageable, pageable.getOffset() + content.size())); + } + + return totalSupplier.map(total -> new PageImpl<>(content, pageable, total)); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java index 1da48bfc8e..1abf4c75d5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java @@ -18,13 +18,23 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import org.bson.Document; +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.util.Assert; import com.querydsl.core.types.EntityPath; @@ -159,6 +169,20 @@ public Mono exists(Predicate predicate) { return createQueryFor(predicate).fetchCount().map(it -> it != 0); } + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor#findBy(com.querydsl.core.types.Predicate, java.util.function.Function) + */ + @Override + public > P findBy(Predicate predicate, + Function, P> queryFunction) { + + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(queryFunction, "Query function must not be null!"); + + return queryFunction.apply(new ReactiveFluentQuerydsl(predicate, (Class) typeInformation().getJavaType())); + } + /** * Creates a {@link ReactiveSpringDataMongodbQuery} for the given {@link Predicate}. * @@ -177,8 +201,8 @@ private ReactiveSpringDataMongodbQuery createQueryFor(Predicate predicate) { private ReactiveSpringDataMongodbQuery createQuery() { Class javaType = typeInformation().getJavaType(); - return new ReactiveSpringDataMongodbQuery<>(mongodbSerializer(), mongoOperations, javaType, - mongoOperations.getCollectionName(javaType)); + return new ReactiveSpringDataMongodbQuery<>(mongoOperations, javaType, javaType, + mongoOperations.getCollectionName(javaType), it -> {}); } /** @@ -194,4 +218,105 @@ private ReactiveSpringDataMongodbQuery applySorting(ReactiveSpringDataMongodb return query; } + /** + * {@link org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery} using Querydsl {@link Predicate}. + * + * @since 3.3 + * @author Mark Paluch + */ + class ReactiveFluentQuerydsl extends ReactiveFluentQuerySupport { + + ReactiveFluentQuerydsl(Predicate predicate, Class resultType) { + this(predicate, Sort.unsorted(), resultType, Collections.emptyList()); + } + + ReactiveFluentQuerydsl(Predicate predicate, Sort sort, Class resultType, List fieldsToInclude) { + super(predicate, sort, resultType, fieldsToInclude); + } + + @Override + protected ReactiveFluentQuerydsl create(Predicate predicate, Sort sort, Class resultType, + List fieldsToInclude) { + return new ReactiveFluentQuerydsl<>(predicate, sort, resultType, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#one() + */ + @Override + public Mono one() { + return createQuery().fetchOne(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#first() + */ + @Override + public Mono first() { + return createQuery().fetchFirst(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#all() + */ + @Override + public Flux all() { + return createQuery().fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#page(org.springframework.data.domain.Pageable) + */ + @Override + public Mono> page(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null!"); + + return createQuery().fetchPage(pageable); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#count() + */ + @Override + public Mono count() { + return createQuery().fetchCount(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#exists() + */ + @Override + public Mono exists() { + return count().map(it -> it > 0).defaultIfEmpty(false); + } + + private ReactiveSpringDataMongodbQuery createQuery() { + + return new ReactiveSpringDataMongodbQuery<>(mongoOperations, typeInformation().getJavaType(), getResultType(), + mongoOperations.getCollectionName(typeInformation().getJavaType()), this::customize).where(getPredicate()); + } + + private void customize(BasicQuery query) { + + List fieldsToInclude = getFieldsToInclude(); + + if (!fieldsToInclude.isEmpty()) { + Document fields = new Document(); + fieldsToInclude.forEach(field -> fields.put(field, 1)); + query.setFieldsObject(fields); + } + + if (getSort().isSorted()) { + query.with(getSort()); + } + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java index 8b30e585e6..d00d8873e5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java @@ -21,11 +21,14 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; import org.bson.Document; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithProjection; +import org.springframework.data.mongodb.core.ReactiveFindOperation; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; @@ -44,7 +47,6 @@ import com.querydsl.core.types.Path; import com.querydsl.core.types.Predicate; import com.querydsl.mongodb.MongodbOps; -import com.querydsl.mongodb.document.MongodbDocumentSerializer; /** * MongoDB query with utilizing {@link ReactiveMongoOperations} for command execution. @@ -59,21 +61,23 @@ class ReactiveSpringDataMongodbQuery extends SpringDataMongodbQuerySupport> { private final ReactiveMongoOperations mongoOperations; - private final FindWithProjection find; + private final Consumer queryCustomizer; + private final ReactiveFindOperation.FindWithQuery find; ReactiveSpringDataMongodbQuery(ReactiveMongoOperations mongoOperations, Class entityClass) { - this(new SpringDataMongodbSerializer(mongoOperations.getConverter()), mongoOperations, entityClass, null); + this(mongoOperations, entityClass, entityClass, null, it -> {}); } @SuppressWarnings("unchecked") - ReactiveSpringDataMongodbQuery(MongodbDocumentSerializer serializer, ReactiveMongoOperations mongoOperations, - Class entityClass, @Nullable String collection) { + ReactiveSpringDataMongodbQuery(ReactiveMongoOperations mongoOperations, Class domainType, + Class resultType, @Nullable String collection, Consumer queryCustomizer) { - super(serializer); + super(new SpringDataMongodbSerializer(mongoOperations.getConverter())); this.mongoOperations = mongoOperations; - this.find = StringUtils.hasText(collection) ? mongoOperations.query((Class) entityClass).inCollection(collection) - : mongoOperations.query((Class) entityClass); + this.queryCustomizer = queryCustomizer; + this.find = (StringUtils.hasText(collection) ? mongoOperations.query(domainType).inCollection(collection) + : mongoOperations.query(domainType)).as((Class) resultType); } /** @@ -86,7 +90,19 @@ Flux fetch() { } /** - * Fetch the first matching query result. + * Fetch all matching query results as page. + * + * @return {@link Mono} emitting the requested page. + */ + Mono> fetchPage(Pageable pageable) { + + Mono> content = createQuery().flatMapMany(it -> find.matching(it).all()).collectList(); + + return content.flatMap(it -> ReactivePageableExecutionUtils.getPage(it, pageable, fetchCount())); + } + + /** + * Fetch the one matching query result. * * @return {@link Mono} emitting the first query result or {@link Mono#empty()} if there are none. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. @@ -95,6 +111,16 @@ Mono fetchOne() { return createQuery().flatMap(it -> find.matching(it).one()); } + /** + * Fetch the first matching query result. @return {@link Mono} emitting the first query result or {@link Mono#empty()} + * if there are none. + * + * @since 3.3 + */ + Mono fetchFirst() { + return createQuery().flatMap(it -> find.matching(it).first()); + } + /** * Fetch the count of matching query results. * @@ -144,6 +170,8 @@ protected Mono createQuery(Mono filter, @Nullable Expression ids) { Assert.notNull(ids, "The given Iterable of ids must not be null!"); - mongoOperations.remove(getIdQuery(ids), entityInformation.getJavaType(), - entityInformation.getCollectionName()); + mongoOperations.remove(getIdQuery(ids), entityInformation.getJavaType(), entityInformation.getCollectionName()); } /* @@ -362,8 +365,8 @@ public Page findAll(Example example, Pageable pageable) { List list = mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName()); - return PageableExecutionUtils.getPage(list, pageable, - () -> mongoOperations.count(Query.of(query).limit(-1).skip(-1), example.getProbeType(), entityInformation.getCollectionName())); + return PageableExecutionUtils.getPage(list, pageable, () -> mongoOperations + .count(Query.of(query).limit(-1).skip(-1), example.getProbeType(), entityInformation.getCollectionName())); } /* @@ -396,6 +399,20 @@ public boolean exists(Example example) { return mongoOperations.exists(query, example.getProbeType(), entityInformation.getCollectionName()); } + /* + * (non-Javadoc) + * @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) { + + Assert.notNull(example, "Sample must not be null!"); + Assert.notNull(queryFunction, "Query function must not be null!"); + + return queryFunction.apply(new FluentQueryByExample<>(example, example.getProbeType())); + } + // ------------------------------------------------------------------------- // Utility methods // ------------------------------------------------------------------------- @@ -410,8 +427,7 @@ private Criteria getIdCriteria(Object id) { private Query getIdQuery(Iterable ids) { - return new Query(new Criteria(entityInformation.getIdAttribute()) - .in(toCollection(ids))); + return new Query(new Criteria(entityInformation.getIdAttribute()).in(toCollection(ids))); } private static Collection toCollection(Iterable ids) { @@ -428,4 +444,119 @@ private List findAll(@Nullable Query query) { return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } + /** + * {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using {@link Example}. + * + * @author Mark Paluch + * @since 3.3 + */ + class FluentQueryByExample extends FetchableFluentQuerySupport, T> { + + FluentQueryByExample(Example example, Class resultType) { + this(example, Sort.unsorted(), resultType, Collections.emptyList()); + } + + FluentQueryByExample(Example example, Sort sort, Class resultType, List fieldsToInclude) { + super(example, sort, resultType, fieldsToInclude); + } + + @Override + protected FluentQueryByExample create(Example predicate, Sort sort, Class resultType, + List fieldsToInclude) { + return new FluentQueryByExample<>(predicate, sort, resultType, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() + */ + @Override + public T oneValue() { + return createQuery().oneValue(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#first() + */ + @Override + public T firstValue() { + return createQuery().firstValue(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() + */ + @Override + public List all() { + return createQuery().all(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) + */ + @Override + public Page page(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null!"); + + List list = createQuery(q -> q.with(pageable)).all(); + + return PageableExecutionUtils.getPage(list, pageable, this::count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() + */ + @Override + public Stream stream() { + return createQuery().stream(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() + */ + @Override + public long count() { + return createQuery().count(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() + */ + @Override + public boolean exists() { + return createQuery().exists(); + } + + private ExecutableFindOperation.TerminatingFind createQuery() { + return createQuery(UnaryOperator.identity()); + } + + private ExecutableFindOperation.TerminatingFind createQuery(UnaryOperator queryCustomizer) { + + Query query = new Query(new Criteria().alike(getPredicate())) // + .collation(entityInformation.getCollation()); + + if (getSort().isSorted()) { + query.with(getSort()); + } + + if (!getFieldsToInclude().isEmpty()) { + query.fields().include(getFieldsToInclude().toArray(new String[0])); + } + + query = queryCustomizer.apply(query); + + return mongoOperations.query(getPredicate().getProbeType()).inCollection(entityInformation.getCollectionName()) + .as(getResultType()).matching(query); + } + + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java index 325547abee..f238f87774 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java @@ -22,19 +22,26 @@ import java.io.Serializable; import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import org.reactivestreams.Publisher; - import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.ReactiveFindOperation; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.util.StreamUtils; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; @@ -210,7 +217,6 @@ public Mono count() { return mongoOperations.count(new Query(), entityInformation.getCollectionName()); } - /* * (non-Javadoc) * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteById(java.lang.Object) @@ -466,6 +472,20 @@ public Mono exists(Example example) { return mongoOperations.exists(query, example.getProbeType(), entityInformation.getCollectionName()); } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.ReactiveQueryByExampleExecutor#findBy(org.springframework.data.domain.Example, java.util.function.Function) + */ + @Override + public > P findBy(Example example, + Function, P> queryFunction) { + + Assert.notNull(example, "Sample must not be null!"); + Assert.notNull(queryFunction, "Query function must not be null!"); + + return queryFunction.apply(new ReactiveFluentQueryByExample<>(example, example.getProbeType())); + } + private Query getIdQuery(Object id) { return new Query(getIdCriteria(id)); } @@ -486,4 +506,110 @@ private static Collection toCollection(Iterable ids) { private Flux findAll(Query query) { return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } + + /** + * {@link org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery} using {@link Example}. + * + * @author Mark Paluch + * @since 3.3 + */ + class ReactiveFluentQueryByExample extends ReactiveFluentQuerySupport, T> { + + ReactiveFluentQueryByExample(Example example, Class resultType) { + this(example, Sort.unsorted(), resultType, Collections.emptyList()); + } + + ReactiveFluentQueryByExample(Example example, Sort sort, Class resultType, List fieldsToInclude) { + super(example, sort, resultType, fieldsToInclude); + } + + @Override + protected ReactiveFluentQueryByExample create(Example predicate, Sort sort, Class resultType, + List fieldsToInclude) { + return new ReactiveFluentQueryByExample<>(predicate, sort, resultType, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#one() + */ + @Override + public Mono one() { + return createQuery().one(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#first() + */ + @Override + public Mono first() { + return createQuery().first(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#all() + */ + @Override + public Flux all() { + return createQuery().all(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#page(org.springframework.data.domain.Pageable) + */ + @Override + public Mono> page(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null!"); + + Mono> items = createQuery(q -> q.with(pageable)).all().collectList(); + + return items.flatMap(content -> ReactivePageableExecutionUtils.getPage(content, pageable, this.count())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#count() + */ + @Override + public Mono count() { + return createQuery().count(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#exists() + */ + @Override + public Mono exists() { + return createQuery().exists(); + } + + private ReactiveFindOperation.TerminatingFind createQuery() { + return createQuery(UnaryOperator.identity()); + } + + private ReactiveFindOperation.TerminatingFind createQuery(UnaryOperator queryCustomizer) { + + Query query = new Query(new Criteria().alike(getPredicate())) // + .collation(entityInformation.getCollation()); + + if (getSort().isSorted()) { + query.with(getSort()); + } + + if (!getFieldsToInclude().isEmpty()) { + query.fields().include(getFieldsToInclude().toArray(new String[0])); + } + + query = queryCustomizer.apply(query); + + return mongoOperations.query(getPredicate().getProbeType()).inCollection(entityInformation.getCollectionName()) + .as(getResultType()).matching(query); + } + + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java index d62aa99c5e..5a914afec1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java @@ -16,14 +16,21 @@ package org.springframework.data.mongodb.repository.support; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; import org.bson.Document; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.core.ExecutableFindOperation; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.support.PageableExecutionUtils; import org.springframework.lang.Nullable; import com.mysema.commons.lang.CloseableIterator; @@ -35,7 +42,6 @@ import com.querydsl.core.types.Expression; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; -import com.querydsl.mongodb.document.MongodbDocumentSerializer; /** * Spring Data specific simple {@link com.querydsl.core.Fetchable} {@link com.querydsl.core.SimpleQuery Query} @@ -48,10 +54,9 @@ public class SpringDataMongodbQuery extends SpringDataMongodbQuerySupport> implements Fetchable { - private final Class entityClass; - private final String collection; private final MongoOperations mongoOperations; - private final ExecutableFindOperation.FindWithProjection find; + private final Consumer queryCustomizer; + private final ExecutableFindOperation.FindWithQuery find; /** * Creates a new {@link SpringDataMongodbQuery}. @@ -72,18 +77,26 @@ public SpringDataMongodbQuery(MongoOperations operations, Class typ */ public SpringDataMongodbQuery(MongoOperations operations, Class type, String collectionName) { - this(new SpringDataMongodbSerializer(operations.getConverter()), operations, type, collectionName); + this(operations, type, type, collectionName, it -> {}); } - private SpringDataMongodbQuery(MongodbDocumentSerializer serializer, MongoOperations operations, - Class type, String collectionName) { - - super(serializer); + /** + * Creates a new {@link SpringDataMongodbQuery}. + * + * @param operations must not be {@literal null}. + * @param domainType must not be {@literal null}. + * @param resultType must not be {@literal null}. + * @param collectionName must not be {@literal null} or empty. + * @since 3.3 + */ + SpringDataMongodbQuery(MongoOperations operations, Class domainType, Class resultType, + String collectionName, Consumer queryCustomizer) { + super(new SpringDataMongodbSerializer(operations.getConverter())); - this.entityClass = (Class) type; - this.collection = collectionName; + Class resultType1 = (Class) resultType; this.mongoOperations = operations; - this.find = mongoOperations.query(this.entityClass).inCollection(collection); + this.queryCustomizer = queryCustomizer; + this.find = mongoOperations.query(domainType).inCollection(collectionName).as(resultType1); } /* @@ -94,19 +107,19 @@ private SpringDataMongodbQuery(MongodbDocumentSerializer serializer, MongoOperat public CloseableIterator iterate() { try { - org.springframework.data.util.CloseableIterator stream = mongoOperations.stream(createQuery(), - entityClass, collection); + Stream stream = stream(); + Iterator iterator = stream.iterator(); return new CloseableIterator() { @Override public boolean hasNext() { - return stream.hasNext(); + return iterator.hasNext(); } @Override public T next() { - return stream.next(); + return iterator.next(); } @Override @@ -124,6 +137,20 @@ public void close() { } } + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#iterable() + */ + @Override + public Stream stream() { + + try { + return find.matching(createQuery()).stream(); + } catch (RuntimeException e) { + return handleException(e, Stream.empty()); + } + } + /* * (non-Javadoc) * @see com.querydsl.core.Fetchable#fetch() @@ -137,6 +164,24 @@ public List fetch() { } } + /** + * Fetch a {@link Page}. + * + * @param pageable + * @return + */ + public Page fetchPage(Pageable pageable) { + + try { + + List content = find.matching(createQuery().with(pageable)).all(); + + return PageableExecutionUtils.getPage(content, pageable, this::fetchCount); + } catch (RuntimeException e) { + return handleException(e, new PageImpl<>(Collections.emptyList(), pageable, 0)); + } + } + /* * (non-Javadoc) * @see com.querydsl.core.Fetchable#fetchFirst() @@ -215,6 +260,8 @@ protected org.springframework.data.mongodb.core.query.Query createQuery(@Nullabl basicQuery.setSortObject(createSort(orderBy)); } + queryCustomizer.accept(basicQuery); + return basicQuery; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java index 0067eb3bf1..176fd175e2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java @@ -40,12 +40,14 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Example; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory; import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; @@ -475,6 +477,150 @@ void findOneByExampleWithoutResultShouldCompleteEmpty() { repository.findOne(example).as(StepVerifier::create).verifyComplete(); } + @Test // GH-3757 + void findByShouldReturnFirstResult() { + + ReactivePerson probe = new ReactivePerson(); + probe.setFirstname(oliver.getFirstname()); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), FluentQuery.ReactiveFluentQuery::first) // + .as(StepVerifier::create) // + .expectNext(oliver) // + .verifyComplete(); + } + + @Test // GH-3757 + void findByShouldReturnOneResult() { + + ReactivePerson probe = new ReactivePerson(); + probe.setFirstname(oliver.getFirstname()); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), FluentQuery.ReactiveFluentQuery::one) // + .as(StepVerifier::create) // + .expectNext(oliver) // + .verifyComplete(); + + probe = new ReactivePerson(); + probe.setLastname(oliver.getLastname()); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), FluentQuery.ReactiveFluentQuery::one) // + .as(StepVerifier::create) // + .verifyError(IncorrectResultSizeDataAccessException.class); + } + + @Test // GH-3757 + void findByShouldReturnAll() { + + ReactivePerson probe = new ReactivePerson(); + probe.setLastname(oliver.getLastname()); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), FluentQuery.ReactiveFluentQuery::all) // + .as(StepVerifier::create) // + .expectNextCount(2) // + .verifyComplete(); + } + + @Test // GH-3757 + void findByShouldApplySortAll() { + + ReactivePerson probe = new ReactivePerson(); + probe.setLastname(oliver.getLastname()); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), it -> it.sortBy(Sort.by("firstname")).all()) // + .as(StepVerifier::create) // + .expectNext(dave, oliver) // + .verifyComplete(); + + repository + .findBy(Example.of(probe, matching().withIgnorePaths("age")), + it -> it.sortBy(Sort.by(Direction.DESC, "firstname")).all()) // + .as(StepVerifier::create) // + .expectNext(oliver, dave) // + .verifyComplete(); + } + + @Test // GH-3757 + void findByShouldApplyProjection() { + + ReactivePerson probe = new ReactivePerson(); + probe.setLastname(oliver.getLastname()); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), it -> it.project("firstname").first()) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.getFirstname()).isNotNull(); + assertThat(it.getLastname()).isNull(); + }).verifyComplete(); + } + + @Test // GH-3757 + void findByShouldApplyPagination() { + + ReactivePerson probe = new ReactivePerson(); + probe.setLastname(oliver.getLastname()); + + repository + .findBy(Example.of(probe, matching().withIgnorePaths("age")), + it -> it.page(PageRequest.of(0, 1, Sort.by("firstname")))) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.getTotalElements()).isEqualTo(2); + assertThat(it.getContent()).contains(dave); + }).verifyComplete(); + + repository + .findBy(Example.of(probe, matching().withIgnorePaths("age")), + it -> it.page(PageRequest.of(1, 1, Sort.by("firstname")))) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.getTotalElements()).isEqualTo(2); + assertThat(it.getContent()).contains(oliver); + }).verifyComplete(); + } + + @Test // GH-3757 + void findByShouldCount() { + + ReactivePerson probe = new ReactivePerson(); + probe.setLastname(oliver.getLastname()); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), FluentQuery.ReactiveFluentQuery::count) // + .as(StepVerifier::create) // + .expectNext(2L) // + .verifyComplete(); + + probe = new ReactivePerson(); + probe.setLastname("foo"); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), FluentQuery.ReactiveFluentQuery::count) // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test // GH-3757 + void findByShouldReportExists() { + + ReactivePerson probe = new ReactivePerson(); + probe.setLastname(oliver.getLastname()); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), FluentQuery.ReactiveFluentQuery::exists) // + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + probe = new ReactivePerson(); + probe.setLastname("foo"); + + repository.findBy(Example.of(probe, matching().withIgnorePaths("age")), FluentQuery.ReactiveFluentQuery::exists) // + .as(StepVerifier::create) // + .expectNext(false) // + .verifyComplete(); + } + interface ReactivePersonRepository extends ReactiveMongoRepository { Flux findByLastname(String lastname); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java index 782e46b134..01f5df84ba 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java @@ -24,9 +24,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -41,6 +43,7 @@ import org.springframework.data.mongodb.repository.QUser; import org.springframework.data.mongodb.repository.User; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -240,4 +243,104 @@ protected MongoDatabase doGetDatabase() { repository.findOne(person.firstname.contains("batman")); } + + @Test // GH-3757 + public void findByShouldReturnFirstResult() { + + Person result = repository.findBy(person.firstname.eq(oliver.getFirstname()), + FluentQuery.FetchableFluentQuery::oneValue); + + assertThat(result).isEqualTo(oliver); + } + + @Test // GH-3757 + public void findByShouldReturnOneResult() { + + Person result = repository.findBy(person.firstname.eq(oliver.getFirstname()), + FluentQuery.FetchableFluentQuery::oneValue); + + assertThat(result).isEqualTo(oliver); + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy( + () -> repository.findBy(person.lastname.eq(oliver.getLastname()), FluentQuery.FetchableFluentQuery::one)); + } + + @Test // GH-3757 + public void findByShouldReturnAll() { + + List result = repository.findBy(person.lastname.eq(oliver.getLastname()), + FluentQuery.FetchableFluentQuery::all); + + assertThat(result).hasSize(2); + } + + @Test // GH-3757 + public void findByShouldApplySortAll() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + List result = repository.findBy(person.lastname.eq(oliver.getLastname()), + it -> it.sortBy(Sort.by("firstname")).all()); + assertThat(result).containsSequence(dave, oliver); + + result = repository.findBy(person.lastname.eq(oliver.getLastname()), + it -> it.sortBy(Sort.by(Sort.Direction.DESC, "firstname")).all()); + assertThat(result).containsSequence(oliver, dave); + } + + @Test // GH-3757 + public void findByShouldApplyProjection() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + Person result = repository.findBy(person.lastname.eq(oliver.getLastname()), + it -> it.project("firstname").firstValue()); + + assertThat(result.getFirstname()).isNotNull(); + assertThat(result.getLastname()).isNull(); + } + + @Test // GH-3757 + public void findByShouldApplyPagination() { + + Page first = repository.findBy(person.lastname.eq(oliver.getLastname()), + it -> it.page(PageRequest.of(0, 1, Sort.by("firstname")))); + assertThat(first.getTotalElements()).isEqualTo(2); + assertThat(first.getContent()).contains(dave); + + Page next = repository.findBy(person.lastname.eq(oliver.getLastname()), + it -> it.page(PageRequest.of(1, 1, Sort.by("firstname")))); + + assertThat(next.getTotalElements()).isEqualTo(2); + assertThat(next.getContent()).contains(oliver); + } + + @Test // GH-3757 + public void findByShouldCount() { + + long count = repository.findBy(person.lastname.eq(oliver.getLastname()), FluentQuery.FetchableFluentQuery::count); + assertThat(count).isEqualTo(2L); + + count = repository.findBy(person.lastname.eq("foo"), FluentQuery.FetchableFluentQuery::count); + assertThat(count).isEqualTo(0L); + } + + @Test // GH-3757 + public void findByShouldReportExists() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + boolean exists = repository.findBy(person.lastname.eq(oliver.getLastname()), + FluentQuery.FetchableFluentQuery::exists); + assertThat(exists).isTrue(); + + probe = new Person(); + probe.setLastname("foo"); + + exists = repository.findBy(person.lastname.eq("foo"), FluentQuery.FetchableFluentQuery::exists); + assertThat(exists).isFalse(); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java index e16a4d7b8c..fbd5a495a8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.repository.support; +import static org.assertj.core.api.Assertions.*; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -34,6 +36,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; @@ -49,6 +52,7 @@ import org.springframework.data.mongodb.repository.User; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.mongodb.test.util.MongoTestUtils; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -269,12 +273,11 @@ public void queryShouldTerminateWithUnsupportedOperationOnJoinWithNoResults() { .as(StepVerifier::create) // .expectNextCount(1) // .verifyComplete(); - ; + operations.save(person2) // .as(StepVerifier::create) // .expectNextCount(1) // .verifyComplete(); - ; Flux result = new ReactiveSpringDataMongodbQuery<>(operations, Person.class).where() .join(person.coworker, QUser.user).on(QUser.user.username.eq("does-not-exist")).fetch(); @@ -330,4 +333,112 @@ protected Mono doGetDatabase() { .expectError(PermissionDeniedDataAccessException.class) // .verify(); } + + @Test // GH-3757 + public void findByShouldReturnFirstResult() { + + repository.findBy(person.firstname.eq(oliver.getFirstname()), FluentQuery.ReactiveFluentQuery::first) // + .as(StepVerifier::create) // + .expectNext(oliver) // + .verifyComplete(); + } + + @Test // GH-3757 + public void findByShouldReturnOneResult() { + + repository.findBy(person.firstname.eq(oliver.getFirstname()), FluentQuery.ReactiveFluentQuery::one) // + .as(StepVerifier::create) // + .expectNext(oliver) // + .verifyComplete(); + + repository.findBy(person.lastname.eq(oliver.getLastname()), FluentQuery.ReactiveFluentQuery::one) // + .as(StepVerifier::create) // + .verifyError(IncorrectResultSizeDataAccessException.class); + } + + @Test // GH-3757 + public void findByShouldReturnAll() { + + repository.findBy(person.lastname.eq(oliver.getLastname()), FluentQuery.ReactiveFluentQuery::all) // + .as(StepVerifier::create) // + .expectNextCount(2) // + .verifyComplete(); + } + + @Test // GH-3757 + public void findByShouldApplySortAll() { + + repository.findBy(person.lastname.eq(oliver.getLastname()), it -> it.sortBy(Sort.by("firstname")).all()) // + .as(StepVerifier::create) // + .expectNext(dave, oliver) // + .verifyComplete(); + + repository + .findBy(person.lastname.eq(oliver.getLastname()), it -> it.sortBy(Sort.by(Direction.DESC, "firstname")).all()) // + .as(StepVerifier::create) // + .expectNext(oliver, dave) // + .verifyComplete(); + } + + @Test // GH-3757 + public void findByShouldApplyProjection() { + + repository.findBy(person.lastname.eq(oliver.getLastname()), it -> it.project("firstname").first()) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.getFirstname()).isNotNull(); + assertThat(it.getLastname()).isNull(); + }).verifyComplete(); + } + + @Test // GH-3757 + public void findByShouldApplyPagination() { + + repository + .findBy(person.lastname.eq(oliver.getLastname()), it -> it.page(PageRequest.of(0, 1, Sort.by("firstname")))) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.getTotalElements()).isEqualTo(2); + assertThat(it.getContent()).contains(dave); + }).verifyComplete(); + + repository + .findBy(person.lastname.eq(oliver.getLastname()), it -> it.page(PageRequest.of(1, 1, Sort.by("firstname")))) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.getTotalElements()).isEqualTo(2); + assertThat(it.getContent()).contains(oliver); + }).verifyComplete(); + } + + @Test // GH-3757 + public void findByShouldCount() { + + repository.findBy(person.lastname.eq(oliver.getLastname()), FluentQuery.ReactiveFluentQuery::count) // + .as(StepVerifier::create) // + .expectNext(2L) // + .verifyComplete(); + + repository.findBy(person.lastname.eq("foo"), FluentQuery.ReactiveFluentQuery::count) // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test // GH-3757 + public void findByShouldReportExists() { + + repository.findBy(person.lastname.eq(oliver.getLastname()), FluentQuery.ReactiveFluentQuery::exists) // + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + repository.findBy(person.lastname.eq("foo"), FluentQuery.ReactiveFluentQuery::exists) // + .as(StepVerifier::create) // + .expectNext(false) // + .verifyComplete(); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index 61cd78ea93..f5e14fdf70 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -31,10 +31,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.MongoTransactionManager; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; @@ -51,6 +54,7 @@ import org.springframework.data.mongodb.test.util.MongoTemplateExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.Template; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.support.TransactionTemplate; @@ -459,6 +463,126 @@ void deleteAllByIds() { .hasSize(all.size() - 2).doesNotContain(dave, carter); } + @Test // GH-3757 + void findByShouldReturnFirstResult() { + + Person probe = new Person(); + probe.setFirstname(oliver.getFirstname()); + + Person result = repository.findBy(Example.of(probe, getMatcher()), FluentQuery.FetchableFluentQuery::firstValue); + + assertThat(result).isEqualTo(oliver); + } + + @Test // GH-3757 + void findByShouldReturnOneResult() { + + Person probe = new Person(); + probe.setFirstname(oliver.getFirstname()); + + Person result = repository.findBy(Example.of(probe, getMatcher()), FluentQuery.FetchableFluentQuery::oneValue); + + assertThat(result).isEqualTo(oliver); + + Person probeByLastname = new Person(); + probeByLastname.setLastname(oliver.getLastname()); + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy( + () -> repository.findBy(Example.of(probeByLastname, getMatcher()), FluentQuery.FetchableFluentQuery::one)); + } + + @Test // GH-3757 + void findByShouldReturnAll() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + List result = repository.findBy(Example.of(probe, getMatcher()), FluentQuery.FetchableFluentQuery::all); + + assertThat(result).hasSize(2); + } + + @Test // GH-3757 + void findByShouldApplySortAll() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + List result = repository.findBy(Example.of(probe, getMatcher()), + it -> it.sortBy(Sort.by("firstname")).all()); + assertThat(result).containsSequence(dave, oliver); + + result = repository.findBy(Example.of(probe, getMatcher()), + it -> it.sortBy(Sort.by(Sort.Direction.DESC, "firstname")).all()); + assertThat(result).containsSequence(oliver, dave); + } + + @Test // GH-3757 + void findByShouldApplyProjection() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + Person result = repository.findBy(Example.of(probe, getMatcher()), it -> it.project("firstname").firstValue()); + + assertThat(result.getFirstname()).isNotNull(); + assertThat(result.getLastname()).isNull(); + } + + @Test // GH-3757 + void findByShouldApplyPagination() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + Page first = repository.findBy(Example.of(probe, getMatcher()), + it -> it.page(PageRequest.of(0, 1, Sort.by("firstname")))); + assertThat(first.getTotalElements()).isEqualTo(2); + assertThat(first.getContent()).contains(dave); + + Page next = repository.findBy(Example.of(probe, getMatcher()), + it -> it.page(PageRequest.of(1, 1, Sort.by("firstname")))); + + assertThat(next.getTotalElements()).isEqualTo(2); + assertThat(next.getContent()).contains(oliver); + } + + @Test // GH-3757 + void findByShouldCount() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + long count = repository.findBy(Example.of(probe, getMatcher()), FluentQuery.FetchableFluentQuery::count); + assertThat(count).isEqualTo(2L); + + probe = new Person(); + probe.setLastname("foo"); + + count = repository.findBy(Example.of(probe, getMatcher()), FluentQuery.FetchableFluentQuery::count); + assertThat(count).isEqualTo(0L); + } + + @Test // GH-3757 + void findByShouldReportExists() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + boolean exists = repository.findBy(Example.of(probe, getMatcher()), FluentQuery.FetchableFluentQuery::exists); + assertThat(exists).isTrue(); + + probe = new Person(); + probe.setLastname("foo"); + + exists = repository.findBy(Example.of(probe, getMatcher()), FluentQuery.FetchableFluentQuery::exists); + assertThat(exists).isFalse(); + } + + private ExampleMatcher getMatcher() { + return matching().withIgnorePaths("age", "createdAt", "sex", "email", "id"); + } + private void assertThatAllReferencePersonsWereStoredCorrectly(Map references, List saved) { for (Person person : saved) {