Skip to content

Commit b996486

Browse files
Introduce Update annotation.
Switch update execution to an annotation based model that allows usage of both the classic update as well as the aggregation pipeline variant. Add the reactive variant of it. Make sure to allow parameter binding for update expressions and verify method return types. Update Javadoc and reference documentation. See: #2107 Original Pull Request: #284
1 parent 28708ce commit b996486

30 files changed

+1021
-240
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.repository;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
import org.springframework.core.annotation.AliasFor;
25+
26+
/**
27+
* Annotation to declare update operators directly on repository methods. Both attributes allow using a placeholder
28+
* notation of {@code ?0}, {@code ?1} and so on. The update will be applied to documents matching the either method name
29+
* derived or annotated query, but not to any custom implementation methods.
30+
*
31+
* @author Christoph Strobl
32+
*/
33+
@Retention(RetentionPolicy.RUNTIME)
34+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
35+
@Documented
36+
public @interface Update {
37+
38+
/**
39+
* Takes a MongoDB JSON string to define the actual update to be executed.
40+
*
41+
* @return the MongoDB JSON string representation of the update. Empty string by default.
42+
* @see #update()
43+
*/
44+
@AliasFor("update")
45+
String value() default "";
46+
47+
/**
48+
* Takes a MongoDB JSON string to define the actual update to be executed.
49+
*
50+
* @return the MongoDB JSON string representation of the update. Empty string by default.
51+
* @see <a href=
52+
* "https://docs.mongodb.com/manual/tutorial/update-documents/">https://docs.mongodb.com/manual/tutorial/update-documents/</a>
53+
*/
54+
@AliasFor("value")
55+
String update() default "";
56+
57+
/**
58+
* Takes a MongoDB JSON string representation of an aggregation pipeline to define the update stages to be executed.
59+
* <p>
60+
* This allows to e.g. define update statement that can evaluate conditionals based on a field value, etc.
61+
*
62+
* @return the MongoDB JSON string representation of the update pipeline. Empty array by default.
63+
* @see <a href=
64+
* "https://docs.mongodb.com/manual/tutorial/update-documents-with-aggregation-pipeline/">https://docs.mongodb.com/manual/tutorial/update-documents-with-aggregation-pipeline</a>
65+
*/
66+
String[] pipeline() default {};
67+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java

+119-62
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,44 @@
1515
*/
1616
package org.springframework.data.mongodb.repository.query;
1717

18+
import java.util.ArrayList;
19+
import java.util.List;
20+
1821
import org.bson.Document;
1922
import org.bson.codecs.configuration.CodecRegistry;
20-
import org.springframework.data.domain.Pageable;
2123
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
2224
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
2325
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
2426
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
27+
import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate;
2528
import org.springframework.data.mongodb.core.MongoOperations;
29+
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
30+
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
31+
import org.springframework.data.mongodb.core.query.BasicUpdate;
2632
import org.springframework.data.mongodb.core.query.Query;
27-
import org.springframework.data.mongodb.core.query.Update;
33+
import org.springframework.data.mongodb.core.query.UpdateDefinition;
34+
import org.springframework.data.mongodb.repository.Update;
2835
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution;
2936
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.GeoNearExecution;
3037
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution;
3138
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagingGeoNearExecution;
3239
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution;
40+
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.UpdateExecution;
41+
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
42+
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
3343
import org.springframework.data.repository.query.ParameterAccessor;
3444
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
3545
import org.springframework.data.repository.query.RepositoryQuery;
3646
import org.springframework.data.repository.query.ResultProcessor;
3747
import org.springframework.data.spel.ExpressionDependencies;
48+
import org.springframework.data.util.Lazy;
3849
import org.springframework.expression.EvaluationContext;
3950
import org.springframework.expression.ExpressionParser;
51+
import org.springframework.lang.NonNull;
4052
import org.springframework.lang.Nullable;
4153
import org.springframework.util.Assert;
54+
import org.springframework.util.ObjectUtils;
55+
import org.springframework.util.StringUtils;
4256

4357
import com.mongodb.client.MongoDatabase;
4458

@@ -55,8 +69,11 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
5569
private final MongoQueryMethod method;
5670
private final MongoOperations operations;
5771
private final ExecutableFind<?> executableFind;
72+
private final ExecutableUpdate<?> executableUpdate;
5873
private final ExpressionParser expressionParser;
5974
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
75+
private final Lazy<ParameterBindingDocumentCodec> codec = Lazy
76+
.of(() -> new ParameterBindingDocumentCodec(getCodecRegistry()));
6077

6178
/**
6279
* Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
@@ -81,6 +98,7 @@ public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, E
8198
Class<?> type = metadata.getCollectionEntity().getType();
8299

83100
this.executableFind = operations.query(type);
101+
this.executableUpdate = operations.update(type);
84102
this.expressionParser = expressionParser;
85103
this.evaluationContextProvider = evaluationContextProvider;
86104
}
@@ -130,7 +148,17 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F
130148

131149
if (isDeleteQuery()) {
132150
return new DeleteExecution(operations, method);
133-
} else if (method.isGeoNearQuery() && method.isPageQuery()) {
151+
}
152+
153+
if (method.isModifyingQuery()) {
154+
if (isLimiting()) {
155+
throw new IllegalStateException(
156+
String.format("Update method must not be limiting. Offending method: %s", method));
157+
}
158+
return new UpdateExecution(executableUpdate, method, () -> createUpdate(accessor), accessor);
159+
}
160+
161+
if (method.isGeoNearQuery() && method.isPageQuery()) {
134162
return new PagingGeoNearExecution(operation, method, accessor, this);
135163
} else if (method.isGeoNearQuery()) {
136164
return new GeoNearExecution(operation, method, accessor);
@@ -139,11 +167,6 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F
139167
} else if (method.isStreamQuery()) {
140168
return q -> operation.matching(q).stream();
141169
} else if (method.isCollectionQuery()) {
142-
143-
if (method.isModifyingQuery()) {
144-
return q -> new UpdatingCollectionExecution(accessor.getPageable(), accessor.getUpdate()).execute(q);
145-
}
146-
147170
return q -> operation.matching(q.with(accessor.getPageable()).with(accessor.getSort())).all();
148171
} else if (method.isPageQuery()) {
149172
return new PagedExecution(operation, accessor.getPageable());
@@ -153,11 +176,6 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F
153176
return q -> operation.matching(q).exists();
154177
} else {
155178
return q -> {
156-
157-
if (method.isModifyingQuery()) {
158-
return new UpdatingSingleEntityExecution(accessor.getUpdate()).execute(q);
159-
}
160-
161179
TerminatingFind<?> find = operation.matching(q);
162180
return isLimiting() ? find.firstValue() : find.oneValue();
163181
};
@@ -217,6 +235,94 @@ protected Query createCountQuery(ConvertingParameterAccessor accessor) {
217235
return applyQueryMetaAttributesWhenPresent(createQuery(accessor));
218236
}
219237

238+
/**
239+
* Retrieves the {@link UpdateDefinition update} from the given
240+
* {@link org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getUpdate() accessor} or creates
241+
* one via by parsing the annotated statement extracted from {@link Update}.
242+
*
243+
* @param accessor never {@literal null}.
244+
* @return the computed {@link UpdateDefinition}.
245+
* @throws IllegalStateException if no update could be found.
246+
* @since 3.4
247+
*/
248+
protected UpdateDefinition createUpdate(ConvertingParameterAccessor accessor) {
249+
250+
if (accessor.getUpdate() != null) {
251+
return accessor.getUpdate();
252+
}
253+
254+
if (method.hasAnnotatedUpdate()) {
255+
256+
Update updateSource = method.getUpdateSource();
257+
if (StringUtils.hasText(updateSource.update())) {
258+
return new BasicUpdate(bindParameters(updateSource.update(), accessor));
259+
}
260+
if (!ObjectUtils.isEmpty(updateSource.pipeline())) {
261+
return AggregationUpdate.from(parseAggregationPipeline(updateSource.pipeline(), accessor));
262+
}
263+
}
264+
265+
throw new IllegalStateException(String.format("No Update provided for method %s.", method));
266+
}
267+
268+
/**
269+
* Parse the given aggregation pipeline stages applying values to placeholders to compute the actual list of
270+
* {@link AggregationOperation operations}.
271+
*
272+
* @param sourcePipeline must not be {@literal null}.
273+
* @param accessor must not be {@literal null}.
274+
* @return the parsed aggregation pipeline.
275+
* @since 3.4
276+
*/
277+
protected List<AggregationOperation> parseAggregationPipeline(String[] sourcePipeline,
278+
ConvertingParameterAccessor accessor) {
279+
280+
List<AggregationOperation> stages = new ArrayList<>(sourcePipeline.length);
281+
for (String source : sourcePipeline) {
282+
stages.add(computePipelineStage(source, accessor));
283+
}
284+
return stages;
285+
}
286+
287+
private AggregationOperation computePipelineStage(String source, ConvertingParameterAccessor accessor) {
288+
return ctx -> ctx.getMappedObject(bindParameters(source, accessor), getQueryMethod().getDomainClass());
289+
}
290+
291+
protected Document decode(String source, ParameterBindingContext bindingContext) {
292+
return getParameterBindingCodec().decode(source, bindingContext);
293+
}
294+
295+
private Document bindParameters(String source, ConvertingParameterAccessor accessor) {
296+
return decode(source, prepareBindingContext(source, accessor));
297+
}
298+
299+
/**
300+
* Create the {@link ParameterBindingContext binding context} used for SpEL evaluation.
301+
*
302+
* @param source the JSON source.
303+
* @param accessor value provider for parameter binding.
304+
* @return never {@literal null}.
305+
* @since 3.4
306+
*/
307+
protected ParameterBindingContext prepareBindingContext(String source, ConvertingParameterAccessor accessor) {
308+
309+
ExpressionDependencies dependencies = getParameterBindingCodec().captureExpressionDependencies(source,
310+
accessor::getBindableValue, expressionParser);
311+
312+
SpELExpressionEvaluator evaluator = getSpELExpressionEvaluatorFor(dependencies, accessor);
313+
return new ParameterBindingContext(accessor::getBindableValue, evaluator);
314+
}
315+
316+
/**
317+
* Obtain the {@link ParameterBindingDocumentCodec} used for parsing JSON expressions.
318+
*
319+
* @return never {@literal null}.
320+
* @since 3.4
321+
*/
322+
protected ParameterBindingDocumentCodec getParameterBindingCodec() {
323+
return codec.get();
324+
}
325+
220326
/**
221327
* Obtain a the {@link EvaluationContext} suitable to evaluate expressions backed by the given dependencies.
222328
*
@@ -278,53 +384,4 @@ protected CodecRegistry getCodecRegistry() {
278384
* @since 2.0.4
279385
*/
280386
protected abstract boolean isLimiting();
281-
282-
/**
283-
* {@link MongoQueryExecution} for collection returning find and update queries.
284-
*
285-
* @author Thomas Darimont
286-
*/
287-
final class UpdatingCollectionExecution implements MongoQueryExecution {
288-
289-
private final Pageable pageable;
290-
private final Update update;
291-
292-
UpdatingCollectionExecution(Pageable pageable, Update update) {
293-
this.pageable = pageable;
294-
this.update = update;
295-
}
296-
297-
@Override
298-
public Object execute(Query query) {
299-
300-
MongoEntityMetadata<?> metadata = method.getEntityInformation();
301-
return operations.findAndModify(query.with(pageable), update, metadata.getJavaType(),
302-
metadata.getCollectionName());
303-
}
304-
}
305-
306-
/**
307-
* {@link MongoQueryExecution} to return a single entity with update.
308-
*
309-
* @author Thomas Darimont
310-
*/
311-
final class UpdatingSingleEntityExecution implements MongoQueryExecution {
312-
313-
private final Update update;
314-
315-
private UpdatingSingleEntityExecution(Update update) {
316-
this.update = update;
317-
}
318-
319-
/*
320-
* (non-Javadoc)
321-
* @see org.springframework.data.mongodb.repository.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.core.query.Query)
322-
*/
323-
@Override
324-
public Object execute(Query query) {
325-
326-
MongoEntityMetadata<?> metadata = method.getEntityInformation();
327-
return operations.findAndModify(query.limit(1), update, metadata.getJavaType(), metadata.getCollectionName());
328-
}
329-
}
330387
}

0 commit comments

Comments
 (0)