1515 */
1616package org .springframework .data .mongodb .repository .query ;
1717
18+ import java .util .ArrayList ;
19+ import java .util .List ;
20+
1821import org .bson .Document ;
1922import org .bson .codecs .configuration .CodecRegistry ;
20- import org .springframework .data .domain .Pageable ;
2123import org .springframework .data .mapping .model .SpELExpressionEvaluator ;
2224import org .springframework .data .mongodb .core .ExecutableFindOperation .ExecutableFind ;
2325import org .springframework .data .mongodb .core .ExecutableFindOperation .FindWithQuery ;
2426import org .springframework .data .mongodb .core .ExecutableFindOperation .TerminatingFind ;
27+ import org .springframework .data .mongodb .core .ExecutableUpdateOperation .ExecutableUpdate ;
2528import 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 ;
2632import 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 ;
2835import org .springframework .data .mongodb .repository .query .MongoQueryExecution .DeleteExecution ;
2936import org .springframework .data .mongodb .repository .query .MongoQueryExecution .GeoNearExecution ;
3037import org .springframework .data .mongodb .repository .query .MongoQueryExecution .PagedExecution ;
3138import org .springframework .data .mongodb .repository .query .MongoQueryExecution .PagingGeoNearExecution ;
3239import 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 ;
3343import org .springframework .data .repository .query .ParameterAccessor ;
3444import org .springframework .data .repository .query .QueryMethodEvaluationContextProvider ;
3545import org .springframework .data .repository .query .RepositoryQuery ;
3646import org .springframework .data .repository .query .ResultProcessor ;
3747import org .springframework .data .spel .ExpressionDependencies ;
48+ import org .springframework .data .util .Lazy ;
3849import org .springframework .expression .EvaluationContext ;
3950import org .springframework .expression .ExpressionParser ;
51+ import org .springframework .lang .NonNull ;
4052import org .springframework .lang .Nullable ;
4153import org .springframework .util .Assert ;
54+ import org .springframework .util .ObjectUtils ;
55+ import org .springframework .util .StringUtils ;
4256
4357import 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