From a6bd0fcea743f63351f1172cf935eca76a4dba94 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 9 Mar 2021 07:55:19 +0100 Subject: [PATCH] Introduce Update annotation. Switch update execution to an annotation based model that allows usage of both the classic update as well as the aggregation pipeline variant. Add the reactive variant of it. Make sure to allow parameter binding for update expressions and verify method return types. Update Javadoc and reference documentation. See: #2107 Original Pull Request: #284 --- .../data/mongodb/repository/Update.java | 67 +++++++ .../repository/query/AbstractMongoQuery.java | 181 ++++++++++++------ .../query/AbstractReactiveMongoQuery.java | 135 ++++++++++++- .../query/ConvertingParameterAccessor.java | 15 +- .../query/MongoParameterAccessor.java | 10 +- .../repository/query/MongoParameters.java | 16 +- .../MongoParametersParameterAccessor.java | 6 +- .../repository/query/MongoQueryExecution.java | 34 ++++ .../repository/query/MongoQueryMethod.java | 72 ++++++- .../mongodb/repository/query/QueryUtils.java | 41 +++- .../query/ReactiveMongoQueryExecution.java | 37 ++++ .../query/ReactiveMongoQueryMethod.java | 59 +++--- .../query/ReactiveStringBasedAggregation.java | 29 +-- .../query/StringBasedAggregation.java | 25 +-- .../query/StringBasedMongoQuery.java | 23 +-- .../support/MongoRepositoryFactory.java | 2 + .../ReactiveMongoRepositoryFactory.java | 2 + ...tractPersonRepositoryIntegrationTests.java | 70 ++++--- .../data/mongodb/repository/Address.java | 28 ++- .../data/mongodb/repository/Person.java | 2 +- .../mongodb/repository/PersonRepository.java | 21 +- .../ReactiveMongoRepositoryTests.java | 106 ++++++++++ .../query/AbstractMongoQueryUnitTests.java | 44 ++++- ...oParametersParameterAccessorUnitTests.java | 29 +++ .../query/MongoParametersUnitTests.java | 22 +++ .../query/MongoQueryMethodUnitTests.java | 84 ++++++-- .../ReactiveMongoQueryMethodUnitTests.java | 37 +++- .../StringBasedAggregationUnitTests.java | 7 - .../query/StubParameterAccessor.java | 4 +- src/main/asciidoc/new-features.adoc | 5 + .../reference/mongo-repositories.adoc | 50 +++++ 31 files changed, 1022 insertions(+), 241 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Update.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Update.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Update.java new file mode 100644 index 000000000..15c408551 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Update.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to declare update operators directly on repository methods. Both attributes allow using a placeholder + * notation of {@code ?0}, {@code ?1} and so on. The update will be applied to documents matching the either method name + * derived or annotated query, but not to any custom implementation methods. + * + * @author Christoph Strobl + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +public @interface Update { + + /** + * Takes a MongoDB JSON string to define the actual update to be executed. + * + * @return the MongoDB JSON string representation of the update. Empty string by default. + * @see #update() + */ + @AliasFor("update") + String value() default ""; + + /** + * Takes a MongoDB JSON string to define the actual update to be executed. + * + * @return the MongoDB JSON string representation of the update. Empty string by default. + * @see https://docs.mongodb.com/manual/tutorial/update-documents/ + */ + @AliasFor("value") + String update() default ""; + + /** + * Takes a MongoDB JSON string representation of an aggregation pipeline to define the update stages to be executed. + *

+ * This allows to e.g. define update statement that can evaluate conditionals based on a field value, etc. + * + * @return the MongoDB JSON string representation of the update pipeline. Empty array by default. + * @see https://docs.mongodb.com/manual/tutorial/update-documents-with-aggregation-pipeline + */ + String[] pipeline() default {}; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index 46cd175d3..158202c9b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -15,30 +15,44 @@ */ package org.springframework.data.mongodb.repository.query; +import java.util.ArrayList; +import java.util.List; + import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; -import org.springframework.data.domain.Pageable; import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; +import org.springframework.data.mongodb.core.query.BasicUpdate; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.Update; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.GeoNearExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagingGeoNearExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.UpdateExecution; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.data.util.Lazy; import org.springframework.expression.EvaluationContext; import org.springframework.expression.ExpressionParser; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; import com.mongodb.client.MongoDatabase; @@ -55,8 +69,11 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { private final MongoQueryMethod method; private final MongoOperations operations; private final ExecutableFind executableFind; + private final ExecutableUpdate executableUpdate; private final ExpressionParser expressionParser; private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final Lazy codec = Lazy + .of(() -> new ParameterBindingDocumentCodec(getCodecRegistry())); /** * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}. @@ -81,6 +98,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { Class type = metadata.getCollectionEntity().getType(); this.executableFind = operations.query(type); + this.executableUpdate = operations.update(type); this.expressionParser = expressionParser; this.evaluationContextProvider = evaluationContextProvider; } @@ -138,7 +156,17 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { if (isDeleteQuery()) { return new DeleteExecution(operations, method); - } else if (method.isGeoNearQuery() && method.isPageQuery()) { + } + + if (method.isModifyingQuery()) { + if (isLimiting()) { + throw new IllegalStateException( + String.format("Update method must not be limiting. Offending method: %s", method)); + } + return new UpdateExecution(executableUpdate, method, () -> createUpdate(accessor), accessor); + } + + if (method.isGeoNearQuery() && method.isPageQuery()) { return new PagingGeoNearExecution(operation, method, accessor, this); } else if (method.isGeoNearQuery()) { return new GeoNearExecution(operation, method, accessor); @@ -147,11 +175,6 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { } else if (method.isStreamQuery()) { return q -> operation.matching(q).stream(); } else if (method.isCollectionQuery()) { - - if (method.isModifyingQuery()) { - return q -> new UpdatingCollectionExecution(accessor.getPageable(), accessor.getUpdate()).execute(q); - } - return q -> operation.matching(q.with(accessor.getPageable()).with(accessor.getSort())).all(); } else if (method.isPageQuery()) { return new PagedExecution(operation, accessor.getPageable()); @@ -161,11 +184,6 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { return q -> operation.matching(q).exists(); } else { return q -> { - - if (method.isModifyingQuery()) { - return new UpdatingSingleEntityExecution(accessor.getUpdate()).execute(q); - } - TerminatingFind find = operation.matching(q); return isLimiting() ? find.firstValue() : find.oneValue(); }; @@ -225,6 +243,94 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { return applyQueryMetaAttributesWhenPresent(createQuery(accessor)); } + /** + * Retrieves the {@link UpdateDefinition update} from the given + * {@link org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getUpdate() accessor} or creates + * one via by parsing the annotated statement extracted from {@link Update}. + * + * @param accessor never {@literal null}. + * @return the computed {@link UpdateDefinition}. + * @throws IllegalStateException if no update could be found. + * @since 3.4 + */ + protected UpdateDefinition createUpdate(ConvertingParameterAccessor accessor) { + + if (accessor.getUpdate() != null) { + return accessor.getUpdate(); + } + + if (method.hasAnnotatedUpdate()) { + + Update updateSource = method.getUpdateSource(); + if (StringUtils.hasText(updateSource.update())) { + return new BasicUpdate(bindParameters(updateSource.update(), accessor)); + } + if (!ObjectUtils.isEmpty(updateSource.pipeline())) { + return AggregationUpdate.from(parseAggregationPipeline(updateSource.pipeline(), accessor)); + } + } + + throw new IllegalStateException(String.format("No Update provided for method %s.", method)); + } + + /** + * Parse the given aggregation pipeline stages applying values to placeholders to compute the actual list of + * {@link AggregationOperation operations}. + * + * @param sourcePipeline must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @return the parsed aggregation pipeline. + * @since 3.4 + */ + protected List parseAggregationPipeline(String[] sourcePipeline, + ConvertingParameterAccessor accessor) { + + List stages = new ArrayList<>(sourcePipeline.length); + for (String source : sourcePipeline) { + stages.add(computePipelineStage(source, accessor)); + } + return stages; + } + + private AggregationOperation computePipelineStage(String source, ConvertingParameterAccessor accessor) { + return ctx -> ctx.getMappedObject(bindParameters(source, accessor), getQueryMethod().getDomainClass()); + } + + protected Document decode(String source, ParameterBindingContext bindingContext) { + return getParameterBindingCodec().decode(source, bindingContext); + } + + private Document bindParameters(String source, ConvertingParameterAccessor accessor) { + return decode(source, prepareBindingContext(source, accessor)); + } + + /** + * Create the {@link ParameterBindingContext binding context} used for SpEL evaluation. + * + * @param source the JSON source. + * @param accessor value provider for parameter binding. + * @return never {@literal null}. + * @since 3.4 + */ + protected ParameterBindingContext prepareBindingContext(String source, ConvertingParameterAccessor accessor) { + + ExpressionDependencies dependencies = getParameterBindingCodec().captureExpressionDependencies(source, + accessor::getBindableValue, expressionParser); + + SpELExpressionEvaluator evaluator = getSpELExpressionEvaluatorFor(dependencies, accessor); + return new ParameterBindingContext(accessor::getBindableValue, evaluator); + } + + /** + * Obtain the {@link ParameterBindingDocumentCodec} used for parsing JSON expressions. + * + * @return never {@literal null}. + * @since 3.4 + */ + protected ParameterBindingDocumentCodec getParameterBindingCodec() { + return codec.get(); + } + /** * Obtain a the {@link EvaluationContext} suitable to evaluate expressions backed by the given dependencies. * @@ -286,53 +392,4 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { * @since 2.0.4 */ protected abstract boolean isLimiting(); - - /** - * {@link MongoQueryExecution} for collection returning find and update queries. - * - * @author Thomas Darimont - */ - final class UpdatingCollectionExecution implements MongoQueryExecution { - - private final Pageable pageable; - private final Update update; - - UpdatingCollectionExecution(Pageable pageable, Update update) { - this.pageable = pageable; - this.update = update; - } - - @Override - public Object execute(Query query) { - - MongoEntityMetadata metadata = method.getEntityInformation(); - return operations.findAndModify(query.with(pageable), update, metadata.getJavaType(), - metadata.getCollectionName()); - } - } - - /** - * {@link MongoQueryExecution} to return a single entity with update. - * - * @author Thomas Darimont - */ - final class UpdatingSingleEntityExecution implements MongoQueryExecution { - - private final Update update; - - private UpdatingSingleEntityExecution(Update update) { - this.update = update; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.core.query.Query) - */ - @Override - public Object execute(Query query) { - - MongoEntityMetadata metadata = method.getEntityInformation(); - return operations.findAndModify(query.limit(1), update, metadata.getJavaType(), metadata.getCollectionName()); - } - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java index a96dbed67..13d06d092 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java @@ -15,12 +15,16 @@ */ package org.springframework.data.mongodb.repository.query; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +import java.util.ArrayList; +import java.util.List; import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; import org.reactivestreams.Publisher; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.SpELExpressionEvaluator; @@ -29,11 +33,20 @@ import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithProje import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ReactiveFindOperation.TerminatingFind; import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; +import org.springframework.data.mongodb.core.query.BasicUpdate; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.Update; import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.DeleteExecution; import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.GeoNearExecution; import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingConverter; import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingExecution; +import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.UpdateExecution; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; @@ -43,6 +56,8 @@ import org.springframework.data.util.TypeInformation; import org.springframework.expression.ExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; import com.mongodb.MongoClientSettings; @@ -59,6 +74,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { private final ReactiveMongoOperations operations; private final EntityInstantiators instantiators; private final FindWithProjection findOperationWithProjection; + private final ReactiveUpdate updateOps; private final ExpressionParser expressionParser; private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider; @@ -89,6 +105,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { Class type = metadata.getCollectionEntity().getType(); this.findOperationWithProjection = operations.query(type); + this.updateOps = operations.update(type); } /* @@ -180,6 +197,14 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { if (isDeleteQuery()) { return new DeleteExecution(operations, method); + } else if (method.isModifyingQuery()) { + + if (isLimiting()) { + throw new IllegalStateException( + String.format("Update method must not be limiting. Offending method: %s", method)); + } + + return new UpdateExecution(updateOps, method, accessor, createUpdate(accessor)); } else if (method.isGeoNearQuery()) { return new GeoNearExecution(operations, accessor, method.getReturnType()); } else if (isTailable(method)) { @@ -261,6 +286,97 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { return createQuery(accessor).map(this::applyQueryMetaAttributesWhenPresent); } + /** + * Retrieves the {@link UpdateDefinition update} from the given + * {@link org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getUpdate() accessor} or creates + * one via by parsing the annotated statement extracted from {@link Update}. + * + * @param accessor never {@literal null}. + * @return the computed {@link UpdateDefinition}. + * @throws IllegalStateException if no update could be found. + * @since 3.4 + */ + protected Mono createUpdate(MongoParameterAccessor accessor) { + + if (accessor.getUpdate() != null) { + return Mono.just(accessor.getUpdate()); + } + + if (method.hasAnnotatedUpdate()) { + Update updateSource = method.getUpdateSource(); + if (StringUtils.hasText(updateSource.update())) { + + String updateJson = updateSource.update(); + return getParameterBindingCodec() // + .flatMap(codec -> expressionEvaluator(updateJson, accessor, codec)) // + .map(it -> decode(it.getT1(), updateJson, accessor, it.getT2())) // + .map(BasicUpdate::fromDocument); + } + if (!ObjectUtils.isEmpty(updateSource.pipeline())) { + return parseAggregationPipeline(updateSource.pipeline(), accessor).map(AggregationUpdate::from); + } + } + + throw new IllegalStateException(String.format("No Update provided for method %s.", method)); + } + + /** + * Parse the given aggregation pipeline stages applying values to placeholders to compute the actual list of + * {@link AggregationOperation operations}. + * + * @param pipeline must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @return the parsed aggregation pipeline. + * @since 3.4 + */ + protected Mono> parseAggregationPipeline(String[] pipeline, + MongoParameterAccessor accessor) { + + return getCodecRegistry().map(ParameterBindingDocumentCodec::new).flatMap(codec -> { + + List> stages = new ArrayList<>(pipeline.length); + for (String source : pipeline) { + stages.add(computePipelineStage(source, accessor, codec)); + } + return Flux.concat(stages).collectList(); + }); + } + + private Mono computePipelineStage(String source, MongoParameterAccessor accessor, + ParameterBindingDocumentCodec codec) { + + return expressionEvaluator(source, accessor, codec).map(it -> { + return ctx -> ctx.getMappedObject(decode(it.getT1(), source, accessor, it.getT2()), + getQueryMethod().getDomainClass()); + }); + } + + private Mono> expressionEvaluator(String source, + MongoParameterAccessor accessor, ParameterBindingDocumentCodec codec) { + + ExpressionDependencies dependencies = codec.captureExpressionDependencies(source, accessor::getBindableValue, + expressionParser); + return getSpelEvaluatorFor(dependencies, accessor).zipWith(Mono.just(codec)); + } + + private Document decode(SpELExpressionEvaluator expressionEvaluator, String source, MongoParameterAccessor accessor, + ParameterBindingDocumentCodec codec) { + + ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue, + expressionEvaluator); + return codec.decode(source, bindingContext); + } + + /** + * Obtain the {@link ParameterBindingDocumentCodec} used for parsing JSON expressions. + * + * @return never {@literal null}. + * @since 3.4 + */ + protected Mono getParameterBindingCodec() { + return getCodecRegistry().map(ParameterBindingDocumentCodec::new); + } + /** * Obtain a {@link Mono publisher} emitting the {@link SpELExpressionEvaluator} suitable to evaluate expressions * backed by the given dependencies. @@ -269,10 +385,27 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { * @param accessor must not be {@literal null}. * @return a {@link Mono} emitting the {@link SpELExpressionEvaluator} when ready. * @since 2.4 + * @deprecated in favor of {@link #getSpelEvaluatorFor(ExpressionDependencies, MongoParameterAccessor)} */ + @Deprecated protected Mono getSpelEvaluatorFor(ExpressionDependencies dependencies, ConvertingParameterAccessor accessor) { + return getSpelEvaluatorFor(dependencies, (MongoParameterAccessor) accessor); + } + + /** + * Obtain a {@link Mono publisher} emitting the {@link SpELExpressionEvaluator} suitable to evaluate expressions + * backed by the given dependencies. + * + * @param dependencies must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @return a {@link Mono} emitting the {@link SpELExpressionEvaluator} when ready. + * @since 3.4 + */ + protected Mono getSpelEvaluatorFor(ExpressionDependencies dependencies, + MongoParameterAccessor accessor) { + return evaluationContextProvider .getEvaluationContextLater(getQueryMethod().getParameters(), accessor.getValues(), dependencies) .map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(expressionParser, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index 91d2efcb3..dc57498ba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -31,7 +31,7 @@ import org.springframework.data.mongodb.core.convert.MongoWriter; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; -import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -155,6 +155,11 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor { return delegate.getCollation(); } + @Override + public UpdateDefinition getUpdate() { + return delegate.getUpdate(); + } + /** * Converts the given value with the underlying {@link MongoWriter}. * @@ -297,12 +302,4 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor { */ Object nextConverted(MongoPersistentProperty property); } - - /* (non-Javadoc) - * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getUpdate() - */ - @Override - public Update getUpdate() { - return delegate.getUpdate(); - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java index 6b42fdafd..2e33cfae5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java @@ -21,6 +21,7 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.lang.Nullable; @@ -77,10 +78,11 @@ public interface MongoParameterAccessor extends ParameterAccessor { Object[] getValues(); /** - * Returns the {@link Update} to be used for findAndUpdate query. + * Returns the {@link Update} to be used for an update execution. * - * @return - * @since 1.7 + * @return {@literal null} if not present. + * @since 3.4 */ - Update getUpdate(); + @Nullable + UpdateDefinition getUpdate(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java index 5a54e587d..397113554 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java @@ -25,7 +25,7 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; -import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Near; import org.springframework.data.mongodb.repository.query.MongoParameters.MongoParameter; import org.springframework.data.repository.query.Parameter; @@ -70,7 +70,7 @@ public class MongoParameters extends Parameters this.rangeIndex = getTypeIndex(parameterTypeInfo, Range.class, Distance.class); this.maxDistanceIndex = this.rangeIndex == -1 ? getTypeIndex(parameterTypeInfo, Distance.class, null) : -1; this.collationIndex = getTypeIndex(parameterTypeInfo, Collation.class, null); - this.updateIndex = parameterTypes.indexOf(Update.class); + this.updateIndex = QueryUtils.indexOfAssignableParameter(UpdateDefinition.class, parameterTypes); int index = findNearIndexInParameters(method); if (index == -1 && isGeoNearMethod) { @@ -200,6 +200,15 @@ public class MongoParameters extends Parameters return collationIndex != null ? collationIndex.intValue() : -1; } + /** + * Returns the index of the {@link UpdateDefinition} parameter or -1 if not present. + * @return -1 if not present. + * @since 3.4 + */ + public int getUpdateIndex() { + return updateIndex; + } + /* * (non-Javadoc) * @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List) @@ -280,7 +289,4 @@ public class MongoParameters extends Parameters } } - public int getUpdateIndex() { - return updateIndex; - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index a21762688..5450bef55 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -22,7 +22,7 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Term; import org.springframework.data.mongodb.core.query.TextCriteria; -import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -156,9 +156,9 @@ public class MongoParametersParameterAccessor extends ParametersParameterAccesso } @Override - public Update getUpdate() { + public UpdateDefinition getUpdate() { int updateIndex = method.getParameters().getUpdateIndex(); - return updateIndex == -1 ? null : (Update) getValue(updateIndex); + return updateIndex == -1 ? null : (UpdateDefinition) getValue(updateIndex); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index 237b87897..fa77a1c92 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.repository.query; import java.util.List; +import java.util.function.Supplier; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -30,9 +31,11 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.ExecutableFindOperation; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -298,4 +301,35 @@ interface MongoQueryExecution { return writeResult.wasAcknowledged() ? writeResult.getDeletedCount() : 0L; } } + + /** + * {@link MongoQueryExecution} updating documents matching the query. + * + * @author Christph Strobl + * @since 3.4 + */ + final class UpdateExecution implements MongoQueryExecution { + + private final ExecutableUpdate updateOps; + private final MongoQueryMethod method; + private Supplier updateDefinitionSupplier; + private final MongoParameterAccessor accessor; + + UpdateExecution(ExecutableUpdate updateOps, MongoQueryMethod method, Supplier updateSupplier, + MongoParameterAccessor accessor) { + + this.updateOps = updateOps; + this.method = method; + this.updateDefinitionSupplier = updateSupplier; + this.accessor = accessor; + } + + @Override + public Object execute(Query query) { + + return updateOps.matching(query.with(accessor.getSort())) // + .apply(updateDefinitionSupplier.get()) // + .all().getModifiedCount(); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index 191bec49a..605241d1d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -30,15 +30,18 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.Tailable; +import org.springframework.data.mongodb.repository.Update; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.util.ReactiveWrappers; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -64,6 +67,7 @@ public class MongoQueryMethod extends QueryMethod { private final Map, Optional> annotationCache; private @Nullable MongoEntityMetadata metadata; + private Lazy isModifying = Lazy.of(this::resolveModifyingQueryIndicators); /** * Creates a new {@link MongoQueryMethod} from the given {@link Method}. @@ -393,6 +397,10 @@ public class MongoQueryMethod extends QueryMethod { return doFindAnnotation(Aggregation.class); } + Optional lookupUpdateAnnotation() { + return doFindAnnotation(Update.class); + } + @SuppressWarnings("unchecked") private Optional doFindAnnotation(Class annotationType) { @@ -402,8 +410,66 @@ public class MongoQueryMethod extends QueryMethod { @Override public boolean isModifyingQuery() { + return isModifying.get(); + } + + private boolean resolveModifyingQueryIndicators() { + return hasAnnotatedUpdate() + || QueryUtils.indexOfAssignableParameter(UpdateDefinition.class, method.getParameterTypes()) != -1; + } + + /** + * @return {@literal true} if {@link Update} annotation is present. + * @since 3.4 + */ + public boolean hasAnnotatedUpdate() { + return lookupUpdateAnnotation().isPresent(); + } + + /** + * @return the {@link Update} or {@literal null} if not present. + * @since 3.4 + */ + public Update getUpdateSource() { + return lookupUpdateAnnotation().get(); + } + + /** + * Verify the actual {@link QueryMethod} is valid in terms of supported return and parameter types. + * + * @since 3.4 + * @throws IllegalStateException + */ + public void verify() { + + if (isModifyingQuery()) { + + if (isCollectionQuery() || isSliceQuery() || isPageQuery() || isGeoNearQuery() || !isNumericOrVoidReturnValue()) { // + throw new IllegalStateException( + String.format("Update method may be void or return a numeric value (the number of updated documents)." + + "Offending method: %s", method)); + } + + if (hasAnnotatedUpdate()) { // must define either an update or an update pipeline + if (!StringUtils.hasText(getUpdateSource().update()) && ObjectUtils.isEmpty(getUpdateSource().pipeline())) { + throw new IllegalStateException( + String.format("Update method must define either 'Update#update' or 'Update#pipeline' attribute. " + + "Offending method: %s", method)); + } + } + } + } + + private boolean isNumericOrVoidReturnValue() { + + Class resultType = getReturnedObjectType(); + if(ReactiveWrappers.usesReactiveType(resultType)) { + resultType = getReturnType().getComponentType().getType(); + } + + boolean isUpdateCountReturnType = ClassUtils.isAssignable(Number.class, resultType); + boolean isVoidReturnType = ClassUtils.isAssignable(Void.class, resultType); - Class[] parameterTypes = this.method.getParameterTypes(); - return parameterTypes.length > 0 && parameterTypes[parameterTypes.length - 1] == Update.class; + return isUpdateCountReturnType || isVoidReturnType; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java index 596364bc2..5cbbc6541 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java @@ -15,6 +15,9 @@ */ package org.springframework.data.mongodb.repository.query; +import java.util.Arrays; +import java.util.List; + import org.aopalliance.intercept.MethodInterceptor; import org.bson.Document; import org.springframework.aop.framework.ProxyFactory; @@ -22,8 +25,8 @@ import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Query} support @@ -84,4 +87,40 @@ class QueryUtils { evaluationContextProvider); return collation == null ? query : query.collation(collation); } + + /** + * Get the first index of the parameter that can be assigned to the given type. + * + * @param type the type to look for. + * @param parameters the actual parameters. + * @return -1 if not found. + * @since 3.4 + */ + static int indexOfAssignableParameter(Class type, Class[] parameters) { + return indexOfAssignableParameter(type, Arrays.asList(parameters)); + } + + /** + * Get the first index of the parameter that can be assigned to the given type. + * + * @param type the type to look for. + * @param parameters the actual parameters. + * @return -1 if not found. + * @since 3.4 + */ + static int indexOfAssignableParameter(Class type, List> parameters) { + + if(parameters.isEmpty()) { + return -1; + } + + int i = 0; + for(Class parameterType : parameters) { + if(ClassUtils.isAssignable(type, parameterType)) { + return i; + } + i++; + } + return -1; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java index cd21171f0..9ac718dd1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java @@ -28,8 +28,10 @@ import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.Point; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.util.ReactiveWrappers; @@ -39,6 +41,8 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import com.mongodb.client.result.UpdateResult; + /** * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a * {@link org.springframework.data.repository.query.QueryMethod} a {@link AbstractReactiveMongoQuery} can be executed in @@ -149,6 +153,39 @@ interface ReactiveMongoQueryExecution { } } + /** + * {@link MongoQueryExecution} updating documents matching the query. + * + * @author Christph Strobl + * @since 3.4 + */ + final class UpdateExecution implements ReactiveMongoQueryExecution { + + private final ReactiveUpdate updateOps; + private final MongoQueryMethod method; + private final MongoParameterAccessor accessor; + private Mono update; + + UpdateExecution(ReactiveUpdate updateOps, ReactiveMongoQueryMethod method, MongoParameterAccessor accessor, + Mono update) { + + this.updateOps = updateOps; + this.method = method; + this.accessor = accessor; + this.update = update; + } + + @Override + public Publisher execute(Query query, Class type, String collection) { + + return update.flatMap(it -> updateOps.inCollection(collection) // + .matching(query.with(accessor.getSort())) // actually we could do it unsorted + .apply(it) // + .all() // + .map(UpdateResult::getModifiedCount)); + } + } + /** * An {@link ReactiveMongoQueryExecution} that wraps the results of the given delegate with the given result * processing. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethod.java index 08c563ae2..5b5ba0ef9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethod.java @@ -66,33 +66,6 @@ public class ReactiveMongoQueryMethod extends MongoQueryMethod { super(method, metadata, projectionFactory, mappingContext); - if (hasParameterOfType(method, Pageable.class)) { - - TypeInformation returnType = ClassTypeInformation.fromReturnTypeOf(method); - - boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType()); - boolean singleWrapperWithWrappedPageableResult = ReactiveWrappers.isSingleValueType(returnType.getType()) - && (PAGE_TYPE.isAssignableFrom(returnType.getRequiredComponentType()) - || SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType())); - - if (singleWrapperWithWrappedPageableResult) { - throw new InvalidDataAccessApiUsageException( - String.format("'%s.%s' must not use sliced or paged execution. Please use Flux.buffer(size, skip).", - ClassUtils.getShortName(method.getDeclaringClass()), method.getName())); - } - - if (!multiWrapper) { - throw new IllegalStateException(String.format( - "Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: %s", - method.toString())); - } - - if (hasParameterOfType(method, Sort.class)) { - throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. " - + "Use sorting capabilities on Pageable instead! Offending method: %s", method.toString())); - } - } - this.method = method; this.isCollectionQuery = Lazy.of(() -> (!(isPageQuery() || isSliceQuery()) && ReactiveWrappers.isMultiValueType(metadata.getReturnType(method).getType()) || super.isCollectionQuery())); @@ -179,4 +152,36 @@ public class ReactiveMongoQueryMethod extends MongoQueryMethod { return false; } + @Override + public void verify() { + + if (hasParameterOfType(method, Pageable.class)) { + + TypeInformation returnType = ClassTypeInformation.fromReturnTypeOf(method); + + boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType()); + boolean singleWrapperWithWrappedPageableResult = ReactiveWrappers.isSingleValueType(returnType.getType()) + && (PAGE_TYPE.isAssignableFrom(returnType.getRequiredComponentType()) + || SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType())); + + if (singleWrapperWithWrappedPageableResult) { + throw new InvalidDataAccessApiUsageException( + String.format("'%s.%s' must not use sliced or paged execution. Please use Flux.buffer(size, skip).", + ClassUtils.getShortName(method.getDeclaringClass()), method.getName())); + } + + if (!multiWrapper) { + throw new IllegalStateException(String.format( + "Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: %s", + method.toString())); + } + + if (hasParameterOfType(method, Sort.class)) { + throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. " + + "Use sorting capabilities on Pageable instead! Offending method: %s", method.toString())); + } + } + + super.verify(); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java index 95e046525..a05be3e2f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java @@ -18,7 +18,6 @@ package org.springframework.data.mongodb.repository.query; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.ArrayList; import java.util.List; import org.bson.Document; @@ -31,11 +30,8 @@ import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.util.json.ParameterBindingContext; -import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ResultProcessor; -import org.springframework.data.spel.ExpressionDependencies; import org.springframework.expression.ExpressionParser; import org.springframework.util.ClassUtils; @@ -122,30 +118,7 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery { } private Mono> computePipeline(ConvertingParameterAccessor accessor) { - - return getCodecRegistry().map(ParameterBindingDocumentCodec::new).flatMap(codec -> { - - String[] sourcePipeline = getQueryMethod().getAnnotatedAggregation(); - - List> stages = new ArrayList<>(sourcePipeline.length); - for (String source : sourcePipeline) { - stages.add(computePipelineStage(source, accessor, codec)); - } - return Flux.concat(stages).collectList(); - }); - } - - private Mono computePipelineStage(String source, ConvertingParameterAccessor accessor, - ParameterBindingDocumentCodec codec) { - - ExpressionDependencies dependencies = codec.captureExpressionDependencies(source, accessor::getBindableValue, - expressionParser); - - return getSpelEvaluatorFor(dependencies, accessor).map(it -> { - - ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue, it); - return ctx -> ctx.getMappedObject(codec.decode(source, bindingContext), getQueryMethod().getDomainClass()); - }); + return parseAggregationPipeline(getQueryMethod().getAnnotatedAggregation(), accessor); } private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java index 713ce308a..e4a38882f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java @@ -24,7 +24,6 @@ import org.bson.Document; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.SliceImpl; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.aggregation.Aggregation; @@ -35,11 +34,8 @@ import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.util.json.ParameterBindingContext; -import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ResultProcessor; -import org.springframework.data.spel.ExpressionDependencies; import org.springframework.expression.ExpressionParser; import org.springframework.util.ClassUtils; @@ -172,26 +168,7 @@ public class StringBasedAggregation extends AbstractMongoQuery { } List computePipeline(MongoQueryMethod method, ConvertingParameterAccessor accessor) { - - ParameterBindingDocumentCodec codec = new ParameterBindingDocumentCodec(getCodecRegistry()); - String[] sourcePipeline = method.getAnnotatedAggregation(); - - List stages = new ArrayList<>(sourcePipeline.length); - for (String source : sourcePipeline) { - stages.add(computePipelineStage(source, accessor, codec)); - } - return stages; - } - - private AggregationOperation computePipelineStage(String source, ConvertingParameterAccessor accessor, - ParameterBindingDocumentCodec codec) { - - ExpressionDependencies dependencies = codec.captureExpressionDependencies(source, accessor::getBindableValue, - expressionParser); - - SpELExpressionEvaluator evaluator = getSpELExpressionEvaluatorFor(dependencies, accessor); - ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue, evaluator); - return ctx -> ctx.getMappedObject(codec.decode(source, bindingContext), getQueryMethod().getDomainClass()); + return parseAggregationPipeline(method.getAnnotatedAggregation(), accessor); } private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index abeef15ec..d6b10db93 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -47,8 +47,6 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { private final String query; private final String fieldSpec; - private final ExpressionParser expressionParser; - private final boolean isCountQuery; private final boolean isExistsQuery; private final boolean isDeleteQuery; @@ -85,7 +83,6 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); this.query = query; - this.expressionParser = expressionParser; this.fieldSpec = method.getFieldSpecification(); if (method.hasAnnotatedQuery()) { @@ -115,10 +112,8 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { @Override protected Query createQuery(ConvertingParameterAccessor accessor) { - ParameterBindingDocumentCodec codec = getParameterBindingCodec(); - - Document queryObject = codec.decode(this.query, getBindingContext(this.query, accessor, codec)); - Document fieldsObject = codec.decode(this.fieldSpec, getBindingContext(this.fieldSpec, accessor, codec)); + Document queryObject = decode(this.query, prepareBindingContext(this.query, accessor)); + Document fieldsObject = decode(this.fieldSpec, prepareBindingContext(this.fieldSpec, accessor)); Query query = new BasicQuery(queryObject, fieldsObject).with(accessor.getSort()); @@ -129,16 +124,6 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { return query; } - private ParameterBindingContext getBindingContext(String json, ConvertingParameterAccessor accessor, - ParameterBindingDocumentCodec codec) { - - ExpressionDependencies dependencies = codec.captureExpressionDependencies(json, accessor::getBindableValue, - expressionParser); - - SpELExpressionEvaluator evaluator = getSpELExpressionEvaluatorFor(dependencies, accessor); - return new ParameterBindingContext(accessor::getBindableValue, evaluator); - } - /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isCountQuery() @@ -179,8 +164,4 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { boolean isDeleteQuery) { return BooleanUtil.countBooleanTrueValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1; } - - private ParameterBindingDocumentCodec getParameterBindingCodec() { - return new ParameterBindingDocumentCodec(getCodecRegistry()); - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 836f04b6e..0047ee7fd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -199,6 +199,8 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { NamedQueries namedQueries) { MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, mappingContext); + queryMethod.verify(); + String namedQueryName = queryMethod.getNamedQueryName(); if (namedQueries.hasQuery(namedQueryName)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 6476550be..fcb46f4d7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -189,6 +189,8 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup NamedQueries namedQueries) { ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method, metadata, factory, mappingContext); + queryMethod.verify(); + String namedQueryName = queryMethod.getNamedQueryName(); if (namedQueries.hasQuery(namedQueryName)) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 658558d3d..e53769442 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.repository; import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; import static org.springframework.data.geo.Metrics.*; import java.util.ArrayList; @@ -40,6 +41,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -375,7 +377,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests { @Test void rejectsDuplicateEmailAddressOnSave() { - assertThat(dave.getEmail()).isEqualTo("dave@dmband.com"); + assumeThat(repository.findById(dave.getId()).map(Person::getEmail)).contains("dave@dmband.com"); Person daveSyer = new Person("Dave", "Syer"); assertThat(daveSyer.getEmail()).isEqualTo("dave@dmband.com"); @@ -1440,7 +1442,8 @@ public abstract class AbstractPersonRepositoryIntegrationTests { @Test // GH-3633 void annotatedQueryWithNullEqualityCheckShouldWork() { - operations.updateFirst(Query.query(Criteria.where("id").is(dave.getId())), Update.update("age", null), Person.class); + operations.updateFirst(Query.query(Criteria.where("id").is(dave.getId())), Update.update("age", null), + Person.class); Person byQueryWithNullEqualityCheck = repository.findByQueryWithNullEqualityCheck(); assertThat(byQueryWithNullEqualityCheck.getId()).isEqualTo(dave.getId()); @@ -1461,7 +1464,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests { assertThat(result).map(Person::getId).containsExactly(josh.getId()); } - @Test //GH-3656 + @Test // GH-3656 void resultProjectionWithOptionalIsExcecutedCorrectly() { carter.setAddress(new Address("batman", "robin", "gotham")); @@ -1474,35 +1477,58 @@ public abstract class AbstractPersonRepositoryIntegrationTests { assertThat(result.getFirstname()).contains("Carter"); } - /** - * @see DATAMONGO-1188 - */ - @Test - public void shouldSupportFindAndModfiyForQueryDerivationWithCollectionResult() { + @Test // GH-2107 + void shouldAllowToUpdateAllElements() { + assertThat(repository.findAndUpdateViaMethodArgAllByLastname("Matthews", new Update().inc("visits", 1337))).isEqualTo(2); + } - List result = repository.findAndModifyByFirstname("Dave", new Update().inc("visits", 42)); + @Test // GH-2107 + void annotatedUpdateIsAppliedCorrectly() { - assertThat(result.size()).isOne(); - assertThat(result.get(0)).isEqualTo(dave); + assertThat(repository.findAndIncrementVisitsByLastname("Matthews", 1337)).isEqualTo(2); + + assertThat(repository.findByLastname("Matthews")).extracting(Person::getVisits).allMatch(it -> it.equals(1337)); + } + + @Test // GH-2107 + void mixAnnotatedUpdateWithAnnotatedQuery() { - Person dave = repository.findById(result.get(0).getId()).get(); + assertThat(repository.updateAllByLastname("Matthews", 1337)).isEqualTo(2); - assertThat(dave.visits).isEqualTo(42); + assertThat(repository.findByLastname("Matthews")).extracting(Person::getVisits).allMatch(it -> it.equals(1337)); } - /** - * @see DATAMONGO-1188 - */ - @Test - public void shouldSupportFindAndModfiyForQueryDerivationWithSingleResult() { + @Test // GH-2107 + void annotatedUpdateWithSpELIsAppliedCorrectly() { + + assertThat(repository.findAndIncrementVisitsUsingSpELByLastname("Matthews", 1337)).isEqualTo(2); - Person result = repository.findOneAndModifyByFirstname("Dave", new Update().inc("visits", 1337)); + assertThat(repository.findByLastname("Matthews")).extracting(Person::getVisits).allMatch(it -> it.equals(1337)); + } + + @Test // GH-2107 + @EnableIfMongoServerVersion(isGreaterThanEqual = "4.2") + void annotatedAggregationUpdateIsAppliedCorrectly() { + + repository.findAndIncrementVisitsViaPipelineByLastname("Matthews", 1337); + + assertThat(repository.findByLastname("Matthews")).extracting(Person::getVisits).allMatch(it -> it.equals(1337)); + } - assertThat(result).isEqualTo(dave); + @Test // GH-2107 + void shouldAllowToUpdateAllElementsWithVoidReturn() { - Person dave = repository.findById(result.getId()).get(); + repository.findAndUpdateViaMethodArgAllByLastname("Matthews", new Update().inc("visits", 1337)); - assertThat(dave.visits).isEqualTo(1337); + assertThat(repository.findByLastname("Matthews")).extracting(Person::getVisits).allMatch(visits -> visits == 1337); } + @Test // GH-2107 + void allowsToUseComplexTypesInUpdate() { + + Address address = new Address("1007 Mountain Drive", "53540", "Gotham"); + + assertThat(repository.findAndPushShippingAddressByEmail(dave.getEmail(), address)).isEqualTo(1); + assertThat(repository.findById(dave.getId()).map(Person::getShippingAddresses)).contains(Collections.singleton(address)); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java index b935815bf..643081e49 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2021 the original author or authors. + * Copyright 2011-2022 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. @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.repository; +import org.springframework.util.ObjectUtils; + import com.querydsl.core.annotations.QueryEmbeddable; /** @@ -83,4 +85,28 @@ public class Address { public void setCity(String city) { this.city = city; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Address address = (Address) o; + + if (!ObjectUtils.nullSafeEquals(street, address.street)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(zipCode, address.zipCode)) { + return false; + } + return ObjectUtils.nullSafeEquals(city, address.city); + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(street); + result = 31 * result + ObjectUtils.nullSafeHashCode(zipCode); + result = 31 * result + ObjectUtils.nullSafeHashCode(city); + return result; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java index ac2437f10..204068e67 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java @@ -48,7 +48,7 @@ public class Person extends Contact { private String firstname; private String lastname; - @Indexed(unique = true, dropDups = true) private String email; + @Indexed(unique = true) private String email; private Integer age; @SuppressWarnings("unused") private Sex sex; Date createdAt; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index b82bcb1c4..f7407d5ca 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -36,7 +36,7 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; import org.springframework.data.mongodb.core.aggregation.AggregationResults; -import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Person.Sex; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.Param; @@ -420,12 +420,27 @@ public interface PersonRepository extends MongoRepository, Query List findByUnwrappedUser(User user); - List findAndModifyByFirstname(String firstname, Update update); + int findAndUpdateViaMethodArgAllByLastname(String lastname, UpdateDefinition update); - Person findOneAndModifyByFirstname(String firstname, Update update); + @Update("{ '$inc' : { 'visits' : ?1 } }") + int findAndIncrementVisitsByLastname(String lastname, int increment); + + @Query("{ 'lastname' : ?0 }") + @Update("{ '$inc' : { 'visits' : ?1 } }") + int updateAllByLastname(String lastname, int increment); + + @Update( pipeline = {"{ '$set' : { 'visits' : { '$add' : [ '$visits', ?1 ] } } }"}) + void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment); + + @Update("{ '$inc' : { 'visits' : ?#{[1]} } }") + int findAndIncrementVisitsUsingSpELByLastname(String lastname, int increment); + + @Update("{ '$push' : { 'shippingAddresses' : ?1 } }") + int findAndPushShippingAddressByEmail(String email, Address address); @Query("{ 'age' : null }") Person findByQueryWithNullEqualityCheck(); List findBySpiritAnimal(User user); + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java index 5e26503e4..29b215fe7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java @@ -23,6 +23,7 @@ import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -44,6 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -58,6 +60,8 @@ import org.springframework.data.mongodb.core.CollectionOptions; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Person.Sex; import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory; import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository; @@ -625,6 +629,90 @@ class ReactiveMongoRepositoryTests { .verifyComplete(); } + @Test // GH-2107 + void shouldAllowToUpdateAllElements() { + repository.findAndUpdateViaMethodArgAllByLastname("Matthews", new Update().inc("visits", 1337)) + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); + } + + @Test // GH-2107 + void mixAnnotatedUpdateWithAnnotatedQuery() { + + repository.updateAllByLastname("Matthews", 1337) + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); + + repository.findByLastname("Matthews") + .map(Person::getVisits) + .as(StepVerifier::create) + .expectNext(1337, 1337) + .verifyComplete(); + } + + @Test // GH-2107 + void annotatedUpdateWithSpELIsAppliedCorrectly() { + + repository.findAndIncrementVisitsUsingSpELByLastname("Matthews", 1337) + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); + + repository.findByLastname("Matthews") + .map(Person::getVisits) + .as(StepVerifier::create) + .expectNext(1337, 1337) + .verifyComplete(); + } + + @Test // GH-2107 + @EnableIfMongoServerVersion(isGreaterThanEqual = "4.2") + void annotatedAggregationUpdateIsAppliedCorrectly() { + + repository.findAndIncrementVisitsViaPipelineByLastname("Matthews", 1337) + .as(StepVerifier::create) + .verifyComplete(); + + repository.findByLastname("Matthews") + .map(Person::getVisits) + .as(StepVerifier::create) + .expectNext(1337, 1337) + .verifyComplete(); + } + + @Test // GH-2107 + void shouldAllowToUpdateAllElementsWithVoidReturn() { + + repository.findAndIncrementVisitsByLastname("Matthews", 1337) + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); + + repository.findByLastname("Matthews") + .map(Person::getVisits) + .as(StepVerifier::create) + .expectNext(1337, 1337) + .verifyComplete(); + } + + @Test // GH-2107 + void allowsToUseComplexTypesInUpdate() { + + Address address = new Address("1007 Mountain Drive", "53540", "Gotham"); + + repository.findAndPushShippingAddressByEmail(dave.getEmail(), address) // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + repository.findById(dave.getId()).map(Person::getShippingAddresses) + .as(StepVerifier::create) + .consumeNextWith(it -> assertThat(it).containsExactly(address)) + .verifyComplete(); + } + interface ReactivePersonRepository extends ReactiveMongoRepository, ReactiveQuerydslPredicateExecutor { @@ -701,6 +789,24 @@ class ReactiveMongoRepositoryTests { Mono deleteCountByLastname(String lastname); Mono deleteSinglePersonByLastname(String lastname); + + Mono findAndUpdateViaMethodArgAllByLastname(String lastname, UpdateDefinition update); + + @org.springframework.data.mongodb.repository.Update("{ '$inc' : { 'visits' : ?1 } }") + Mono findAndIncrementVisitsByLastname(String lastname, int increment); + + @Query("{ 'lastname' : ?0 }") + @org.springframework.data.mongodb.repository.Update("{ '$inc' : { 'visits' : ?1 } }") + Mono updateAllByLastname(String lastname, int increment); + + @org.springframework.data.mongodb.repository.Update( pipeline = {"{ '$set' : { 'visits' : { '$add' : [ '$visits', ?1 ] } } }"}) + Mono findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment); + + @org.springframework.data.mongodb.repository.Update("{ '$inc' : { 'visits' : ?#{[1]} } }") + Mono findAndIncrementVisitsUsingSpELByLastname(String lastname, int increment); + + @org.springframework.data.mongodb.repository.Update("{ '$push' : { 'shippingAddresses' : ?1 } }") + Mono findAndPushShippingAddressByEmail(String email, Address address); } interface ReactiveContactRepository extends ReactiveMongoRepository {} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java index 92c99185e..d513b35d1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2022 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. @@ -25,6 +25,7 @@ import java.util.Locale; import java.util.Optional; import org.bson.Document; +import org.bson.codecs.configuration.CodecRegistry; import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,7 +36,6 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -45,6 +45,10 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; +import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; +import org.springframework.data.mongodb.core.ExecutableUpdateOperation.TerminatingUpdate; +import org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithQuery; +import org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithUpdate; import org.springframework.data.mongodb.core.MongoExceptionTranslator; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.Person; @@ -56,8 +60,10 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Update; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; @@ -65,7 +71,9 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.expression.spel.standard.SpelExpressionParser; +import com.mongodb.MongoClientSettings; import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.UpdateResult; /** * Unit tests for {@link AbstractMongoQuery}. @@ -82,9 +90,14 @@ class AbstractMongoQueryUnitTests { @Mock MongoOperations mongoOperationsMock; @Mock ExecutableFind executableFind; @Mock FindWithQuery withQueryMock; + @Mock ExecutableUpdate executableUpdate; + @Mock UpdateWithQuery updateWithQuery; + @Mock UpdateWithUpdate updateWithUpdate; + @Mock TerminatingUpdate terminatingUpdate; @Mock BasicMongoPersistentEntity persitentEntityMock; @Mock MongoMappingContext mappingContextMock; @Mock DeleteResult deleteResultMock; + @Mock UpdateResult updateResultMock; @BeforeEach void setUp() { @@ -104,8 +117,12 @@ class AbstractMongoQueryUnitTests { doReturn(executableFind).when(mongoOperationsMock).query(any()); doReturn(withQueryMock).when(executableFind).as(any()); doReturn(withQueryMock).when(withQueryMock).matching(any(Query.class)); + doReturn(executableUpdate).when(mongoOperationsMock).update(any()); + doReturn(updateWithQuery).when(executableUpdate).matching(any(Query.class)); + doReturn(terminatingUpdate).when(updateWithQuery).apply(any(UpdateDefinition.class)); when(mongoOperationsMock.remove(any(), any(), anyString())).thenReturn(deleteResultMock); + when(mongoOperationsMock.updateMulti(any(), any(), any(), anyString())).thenReturn(updateResultMock); } @Test // DATAMONGO-566 @@ -437,6 +454,21 @@ class AbstractMongoQueryUnitTests { .contains(Collation.of("en_US").toDocument()); } + @Test // GH-2107 + void updateExecutionCallsUpdateAllCorrectly() { + + when(terminatingUpdate.all()).thenReturn(updateResultMock); + + createQueryForMethod("findAndIncreaseVisitsByLastname", String.class, int.class) // + .execute(new Object[] { "dalinar", 100 }); + + ArgumentCaptor update = ArgumentCaptor.forClass(UpdateDefinition.class); + verify(updateWithQuery).apply(update.capture()); + verify(terminatingUpdate).all(); + + assertThat(update.getValue().getUpdateObject()).isEqualTo(Document.parse("{ '$inc' : { 'visits' : 100 } }")); + } + private MongoQueryFake createQueryForMethod(String methodName, Class... paramTypes) { return createQueryForMethod(Repo.class, methodName, paramTypes); } @@ -500,6 +532,11 @@ class AbstractMongoQueryUnitTests { isLimitingQuery = limitingQuery; return this; } + + @Override + protected CodecRegistry getCodecRegistry() { + return MongoClientSettings.getDefaultCodecRegistry(); + } } private interface Repo extends MongoRepository { @@ -546,6 +583,9 @@ class AbstractMongoQueryUnitTests { @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }") List findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation); + + @Update("{ '$inc' : { 'visits' : ?1 } }") + void findAndIncreaseVisitsByLastname(String lastname, int value); } // DATAMONGO-1872 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java index e995bc5d5..6dfdd4b06 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java @@ -30,6 +30,8 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -126,6 +128,31 @@ public class MongoParametersParameterAccessorUnitTests { assertThat(accessor.getCollation()).isEqualTo(collation); } + @Test // GH-2107 + public void shouldReturnUpdateIfPresent() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findAndModifyByFirstname", String.class, UpdateDefinition.class); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); + + Update update = new Update(); + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, + new Object[] { "dalinar", update }); + + assertThat(accessor.getUpdate()).isSameAs(update); + } + + @Test // GH-2107 + public void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); + + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, + new Object[] { new Point(0,0) }); + + assertThat(accessor.getUpdate()).isNull(); + } + interface PersonRepository extends Repository { List findByLocationNear(Point point); @@ -138,5 +165,7 @@ public class MongoParametersParameterAccessorUnitTests { List findByFirstname(String firstname, Collation collation); + List findAndModifyByFirstname(String firstname, UpdateDefinition update); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java index 00f488d00..5b82a711e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java @@ -25,12 +25,14 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Near; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.repository.query.Parameter; @@ -163,6 +165,24 @@ class MongoParametersUnitTests { assertThat(parameters.getCollationParameterIndex()).isOne(); } + @Test // GH-2107 + void shouldReturnIndexUpdateIfExists() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findAndModifyByFirstname", String.class, UpdateDefinition.class, Pageable.class); + MongoParameters parameters = new MongoParameters(method, false); + + assertThat(parameters.getUpdateIndex()).isOne(); + } + + @Test // GH-2107 + void shouldReturnInvalidIndexIfUpdateDoesNotExist() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("someOtherMethod", Point.class, Point.class); + MongoParameters parameters = new MongoParameters(method, false); + + assertThat(parameters.getUpdateIndex()).isEqualTo(-1); + } + interface PersonRepository { List findByLocationNear(Point point, Distance distance); @@ -182,5 +202,7 @@ class MongoParametersUnitTests { List findByLocationNear(Point point, Range range); List findByText(String text, Collation collation); + + List findAndModifyByFirstname(String firstname, UpdateDefinition update, Pageable page); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java index a9c142f10..ddafe217a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java @@ -21,9 +21,8 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.List; -import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.data.domain.Pageable; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoPage; @@ -31,7 +30,10 @@ import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.User; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Address; import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.Contact; @@ -53,7 +55,7 @@ public class MongoQueryMethodUnitTests { MongoMappingContext context; - @Before + @BeforeEach public void setUp() { context = new MongoMappingContext(); } @@ -105,13 +107,13 @@ public class MongoQueryMethodUnitTests { .isThrownBy(() -> queryMethod(PersonRepository.class, "findByLocationNear", Point.class, Distance.class)); } - @Test(expected = IllegalArgumentException.class) + @Test public void rejectsNullMappingContext() throws Exception { Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Point.class); - new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), - new SpelAwareProxyProjectionFactory(), null); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new MongoQueryMethod(method, + new DefaultRepositoryMetadata(PersonRepository.class), new SpelAwareProxyProjectionFactory(), null)); } @Test @@ -218,8 +220,8 @@ public class MongoQueryMethodUnitTests { MongoQueryMethod method = queryMethod(PersonRepository.class, "findByAggregation"); - Assertions.assertThat(method.hasAnnotatedAggregation()).isTrue(); - Assertions.assertThat(method.getAnnotatedAggregation()).hasSize(1); + assertThat(method.hasAnnotatedAggregation()).isTrue(); + assertThat(method.getAnnotatedAggregation()).hasSize(1); } @Test // DATAMONGO-2153 @@ -227,8 +229,53 @@ public class MongoQueryMethodUnitTests { MongoQueryMethod method = queryMethod(PersonRepository.class, "findByAggregationWithCollation"); - Assertions.assertThat(method.hasAnnotatedCollation()).isTrue(); - Assertions.assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT"); + assertThat(method.hasAnnotatedCollation()).isTrue(); + assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT"); + } + + @Test // GH-2107 + void detectsModifyingQueryByUpdateType() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "findAndUpdateBy", String.class, Update.class); + + assertThat(method.isModifyingQuery()).isTrue(); + } + + @Test // GH-2107 + void detectsModifyingQueryByUpdateDefinitionType() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "findAndUpdateBy", String.class, + UpdateDefinition.class); + + assertThat(method.isModifyingQuery()).isTrue(); + } + + @Test // GH-2107 + void detectsModifyingQueryByAggregationUpdateDefinitionType() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "findAndUpdateBy", String.class, + AggregationUpdate.class); + + assertThat(method.isModifyingQuery()).isTrue(); + } + + @Test // GH-2107 + void queryCreationFailsOnInvalidUpdate() throws Exception { + + assertThatExceptionOfType(IllegalStateException.class) // + .isThrownBy(() -> queryMethod(InvalidUpdateMethodRepo.class, "findAndUpdateByLastname", String.class).verify()) // + .withMessageContaining("Update") // + .withMessageContaining("findAndUpdateByLastname"); + } + + @Test // GH-2107 + void queryCreationForUpdateMethodFailsOnInvalidReturnType() throws Exception { + + assertThatExceptionOfType(IllegalStateException.class) // + .isThrownBy(() -> queryMethod(InvalidUpdateMethodRepo.class, "findAndIncrementVisitsByFirstname", String.class).verify()) // + .withMessageContaining("Update") // + .withMessageContaining("numeric") // + .withMessageContaining("findAndIncrementVisitsByFirstname"); } private MongoQueryMethod queryMethod(Class repository, String name, Class... parameters) throws Exception { @@ -285,6 +332,12 @@ public class MongoQueryMethodUnitTests { @Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }", collation = "de_AT") List findByAggregationWithCollation(); + + void findAndUpdateBy(String firstname, Update update); + + void findAndUpdateBy(String firstname, UpdateDefinition update); + + void findAndUpdateBy(String firstname, AggregationUpdate update); } interface SampleRepository extends Repository { @@ -299,6 +352,15 @@ public class MongoQueryMethodUnitTests { Customer methodReturningAnInterface(); } + interface InvalidUpdateMethodRepo extends Repository { + + @org.springframework.data.mongodb.repository.Update + void findAndUpdateByLastname(String lastname); + + @org.springframework.data.mongodb.repository.Update("{ '$inc' : { 'visits' : 1 } }") + Person findAndIncrementVisitsByFirstname(String firstname); + } + interface Customer { } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java index 12fffca0f..ff2c217e6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2022 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. @@ -50,6 +50,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat * Unit test for {@link ReactiveMongoQueryMethod}. * * @author Mark Paluch + * @author Christoph Strobl */ public class ReactiveMongoQueryMethodUnitTests { @@ -113,7 +114,7 @@ public class ReactiveMongoQueryMethodUnitTests { @Test // DATAMONGO-1444 public void rejectsMonoPageableResult() { assertThatIllegalStateException() - .isThrownBy(() -> queryMethod(PersonRepository.class, "findMonoByLastname", String.class, Pageable.class)); + .isThrownBy(() -> queryMethod(PersonRepository.class, "findMonoByLastname", String.class, Pageable.class).verify()); } @Test // DATAMONGO-1444 @@ -142,13 +143,13 @@ public class ReactiveMongoQueryMethodUnitTests { @Test // DATAMONGO-1444 public void throwsExceptionOnWrappedPage() { assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> queryMethod(PersonRepository.class, "findMonoPageByLastname", String.class, Pageable.class)); + .isThrownBy(() -> queryMethod(PersonRepository.class, "findMonoPageByLastname", String.class, Pageable.class).verify()); } @Test // DATAMONGO-1444 public void throwsExceptionOnWrappedSlice() { assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> queryMethod(PersonRepository.class, "findMonoSliceByLastname", String.class, Pageable.class)); + .isThrownBy(() -> queryMethod(PersonRepository.class, "findMonoSliceByLastname", String.class, Pageable.class).verify()); } @Test // DATAMONGO-1444 @@ -177,6 +178,25 @@ public class ReactiveMongoQueryMethodUnitTests { Assertions.assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT"); } + @Test // GH-2107 + public void queryCreationFailsOnInvalidUpdate() throws Exception { + + assertThatExceptionOfType(IllegalStateException.class) // + .isThrownBy(() -> queryMethod(InvalidUpdateMethodRepo.class, "findAndUpdateByLastname", String.class).verify()) // + .withMessageContaining("Update") // + .withMessageContaining("findAndUpdateByLastname"); + } + + @Test // GH-2107 + public void queryCreationForUpdateMethodFailsOnInvalidReturnType() throws Exception { + + assertThatExceptionOfType(IllegalStateException.class) // + .isThrownBy(() -> queryMethod(InvalidUpdateMethodRepo.class, "findAndIncrementVisitsByFirstname", String.class).verify()) // + .withMessageContaining("Update") // + .withMessageContaining("numeric") // + .withMessageContaining("findAndIncrementVisitsByFirstname"); + } + private ReactiveMongoQueryMethod queryMethod(Class repository, String name, Class... parameters) throws Exception { @@ -232,5 +252,14 @@ public class ReactiveMongoQueryMethodUnitTests { Customer methodReturningAnInterface(); } + interface InvalidUpdateMethodRepo extends Repository { + + @org.springframework.data.mongodb.repository.Update + Mono findAndUpdateByLastname(String lastname); + + @org.springframework.data.mongodb.repository.Update("{ '$inc' : { 'visits' : 1 } }") + Mono findAndIncrementVisitsByFirstname(String firstname); + } + interface Customer {} } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java index 995442f0f..2b5ca6031 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java @@ -262,13 +262,6 @@ public class StringBasedAggregationUnitTests { assertThat(result).isInstanceOf(Stream.class); } - @Test // DATAMONGO-2557 - void aggregationRetrievesCodecFromDriverJustOnceForMultipleAggregationOperationsInPipeline() { - - executeAggregation("multiOperationPipeline", "firstname"); - verify(operations).execute(any()); - } - @Test // DATAMONGO-2506 void aggregateRaisesErrorOnInvalidReturnType() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java index 4c0ef87a6..2414b9f15 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java @@ -28,7 +28,7 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.convert.MongoWriter; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; -import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.lang.Nullable; @@ -175,7 +175,7 @@ class StubParameterAccessor implements MongoParameterAccessor { } @Override - public Update getUpdate() { + public UpdateDefinition getUpdate() { return null; } } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index ddfa1e96e..91b4708b0 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -1,6 +1,11 @@ [[new-features]] = New & Noteworthy +[[new-features.3.4]] +== What's New in Spring Data MongoDB 3.4 + +* Find and update ``Document``s via <>. + [[new-features.3.3]] == What's New in Spring Data MongoDB 3.3 diff --git a/src/main/asciidoc/reference/mongo-repositories.adoc b/src/main/asciidoc/reference/mongo-repositories.adoc index 4d69ee95d..867b7edf4 100644 --- a/src/main/asciidoc/reference/mongo-repositories.adoc +++ b/src/main/asciidoc/reference/mongo-repositories.adoc @@ -289,6 +289,56 @@ lower / upper bounds (`$gt` / `$gte` & `$lt` / `$lte`) according to `Range` NOTE: If the property criterion compares a document, the order of the fields and exact equality in the document matters. +[[mongodb.repositories.queries.update]] +=== Repository Update Methods + +The keywords in the preceding table can also be used to create queries that identify matching documents for running updates on them. +The actual update action is defined via the `@Update` annotation on the method itself as shown in the snippet below. + +Please note that the naming schema for derived queries starts with `find`. +Using _update_ (as in `updateAllByLastname(...)`) is only allowed in combination with `@Query`. + +The update is applied to *all* matching documents and it is *not* possible to limit the scope by passing in a `Page` nor using any of the <>. + +The return type can be either `void` or a _numeric_ type, such as `long` which holds the number of modified documents. + +.Update Methods +==== +[source,java] +---- +public interface PersonRepository extends CrudRepository { + + @Update("{ '$inc' : { 'visits' : 1 } }") + long findAndIncrementVisitsByLastname(String lastname); <1> + + @Update("{ '$inc' : { 'visits' : ?1 } }") + void findAndIncrementVisitsByLastname(String lastname, int increment); <2> + + @Update("{ '$inc' : { 'visits' : ?#{[1]} } }") + long findAndIncrementVisitsUsingSpELByLastname(String lastname, int increment); <3> + + @Update(pipeline = {"{ '$set' : { 'visits' : { '$add' : [ '$visits', ?1 ] } } }"}) + void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment); <4> + + @Update("{ '$push' : { 'shippingAddresses' : ?1 } }") + long findAndPushShippingAddressByEmail(String email, Address address); <5> + + @Query("{ 'lastname' : ?0 }") + @Update("{ '$inc' : { 'visits' : ?1 } }") + void updateAllByLastname(String lastname, int increment); <6> +} +---- +<1> The filter query for the update is derived from the method name. The update is as is and does not bind any parameters. +<2> The actual increment value is defined by the _increment_ method argument that is bound to the `?1` placeholder. +<3> It is possible to use SpEL for parameter binding. +<4> Use the `pipeline` attribute to issue <>. +<5> The update may contain complex objects. +<6> Combine a <> with an update. +==== + +[WARNING] +==== +Repository updates do not emit persistence nor mapping lifecycle events. +==== + [[mongodb.repositories.queries.delete]] === Repository Delete Queries