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