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 }
@@ -138,7 +156,17 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F
138156
139157 if (isDeleteQuery ()) {
140158 return new DeleteExecution (operations , method );
141- } else if (method .isGeoNearQuery () && method .isPageQuery ()) {
159+ }
160+
161+ if (method .isModifyingQuery ()) {
162+ if (isLimiting ()) {
163+ throw new IllegalStateException (
164+ String .format ("Update method must not be limiting. Offending method: %s" , method ));
165+ }
166+ return new UpdateExecution (executableUpdate , method , () -> createUpdate (accessor ), accessor );
167+ }
168+
169+ if (method .isGeoNearQuery () && method .isPageQuery ()) {
142170 return new PagingGeoNearExecution (operation , method , accessor , this );
143171 } else if (method .isGeoNearQuery ()) {
144172 return new GeoNearExecution (operation , method , accessor );
@@ -147,11 +175,6 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F
147175 } else if (method .isStreamQuery ()) {
148176 return q -> operation .matching (q ).stream ();
149177 } else if (method .isCollectionQuery ()) {
150-
151- if (method .isModifyingQuery ()) {
152- return q -> new UpdatingCollectionExecution (accessor .getPageable (), accessor .getUpdate ()).execute (q );
153- }
154-
155178 return q -> operation .matching (q .with (accessor .getPageable ()).with (accessor .getSort ())).all ();
156179 } else if (method .isPageQuery ()) {
157180 return new PagedExecution (operation , accessor .getPageable ());
@@ -161,11 +184,6 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F
161184 return q -> operation .matching (q ).exists ();
162185 } else {
163186 return q -> {
164-
165- if (method .isModifyingQuery ()) {
166- return new UpdatingSingleEntityExecution (accessor .getUpdate ()).execute (q );
167- }
168-
169187 TerminatingFind <?> find = operation .matching (q );
170188 return isLimiting () ? find .firstValue () : find .oneValue ();
171189 };
@@ -225,6 +243,94 @@ protected Query createCountQuery(ConvertingParameterAccessor accessor) {
225243 return applyQueryMetaAttributesWhenPresent (createQuery (accessor ));
226244 }
227245
246+ /**
247+ * Retrieves the {@link UpdateDefinition update} from the given
248+ * {@link org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getUpdate() accessor} or creates
249+ * one via by parsing the annotated statement extracted from {@link Update}.
250+ *
251+ * @param accessor never {@literal null}.
252+ * @return the computed {@link UpdateDefinition}.
253+ * @throws IllegalStateException if no update could be found.
254+ * @since 3.4
255+ */
256+ protected UpdateDefinition createUpdate (ConvertingParameterAccessor accessor ) {
257+
258+ if (accessor .getUpdate () != null ) {
259+ return accessor .getUpdate ();
260+ }
261+
262+ if (method .hasAnnotatedUpdate ()) {
263+
264+ Update updateSource = method .getUpdateSource ();
265+ if (StringUtils .hasText (updateSource .update ())) {
266+ return new BasicUpdate (bindParameters (updateSource .update (), accessor ));
267+ }
268+ if (!ObjectUtils .isEmpty (updateSource .pipeline ())) {
269+ return AggregationUpdate .from (parseAggregationPipeline (updateSource .pipeline (), accessor ));
270+ }
271+ }
272+
273+ throw new IllegalStateException (String .format ("No Update provided for method %s." , method ));
274+ }
275+
276+ /**
277+ * Parse the given aggregation pipeline stages applying values to placeholders to compute the actual list of
278+ * {@link AggregationOperation operations}.
279+ *
280+ * @param sourcePipeline must not be {@literal null}.
281+ * @param accessor must not be {@literal null}.
282+ * @return the parsed aggregation pipeline.
283+ * @since 3.4
284+ */
285+ protected List <AggregationOperation > parseAggregationPipeline (String [] sourcePipeline ,
286+ ConvertingParameterAccessor accessor ) {
287+
288+ List <AggregationOperation > stages = new ArrayList <>(sourcePipeline .length );
289+ for (String source : sourcePipeline ) {
290+ stages .add (computePipelineStage (source , accessor ));
291+ }
292+ return stages ;
293+ }
294+
295+ private AggregationOperation computePipelineStage (String source , ConvertingParameterAccessor accessor ) {
296+ return ctx -> ctx .getMappedObject (bindParameters (source , accessor ), getQueryMethod ().getDomainClass ());
297+ }
298+
299+ protected Document decode (String source , ParameterBindingContext bindingContext ) {
300+ return getParameterBindingCodec ().decode (source , bindingContext );
301+ }
302+
303+ private Document bindParameters (String source , ConvertingParameterAccessor accessor ) {
304+ return decode (source , prepareBindingContext (source , accessor ));
305+ }
306+
307+ /**
308+ * Create the {@link ParameterBindingContext binding context} used for SpEL evaluation.
309+ *
310+ * @param source the JSON source.
311+ * @param accessor value provider for parameter binding.
312+ * @return never {@literal null}.
313+ * @since 3.4
314+ */
315+ protected ParameterBindingContext prepareBindingContext (String source , ConvertingParameterAccessor accessor ) {
316+
317+ ExpressionDependencies dependencies = getParameterBindingCodec ().captureExpressionDependencies (source ,
318+ accessor ::getBindableValue , expressionParser );
319+
320+ SpELExpressionEvaluator evaluator = getSpELExpressionEvaluatorFor (dependencies , accessor );
321+ return new ParameterBindingContext (accessor ::getBindableValue , evaluator );
322+ }
323+
324+ /**
325+ * Obtain the {@link ParameterBindingDocumentCodec} used for parsing JSON expressions.
326+ *
327+ * @return never {@literal null}.
328+ * @since 3.4
329+ */
330+ protected ParameterBindingDocumentCodec getParameterBindingCodec () {
331+ return codec .get ();
332+ }
333+
228334 /**
229335 * Obtain a the {@link EvaluationContext} suitable to evaluate expressions backed by the given dependencies.
230336 *
@@ -286,53 +392,4 @@ protected CodecRegistry getCodecRegistry() {
286392 * @since 2.0.4
287393 */
288394 protected abstract boolean isLimiting ();
289-
290- /**
291- * {@link MongoQueryExecution} for collection returning find and update queries.
292- *
293- * @author Thomas Darimont
294- */
295- final class UpdatingCollectionExecution implements MongoQueryExecution {
296-
297- private final Pageable pageable ;
298- private final Update update ;
299-
300- UpdatingCollectionExecution (Pageable pageable , Update update ) {
301- this .pageable = pageable ;
302- this .update = update ;
303- }
304-
305- @ Override
306- public Object execute (Query query ) {
307-
308- MongoEntityMetadata <?> metadata = method .getEntityInformation ();
309- return operations .findAndModify (query .with (pageable ), update , metadata .getJavaType (),
310- metadata .getCollectionName ());
311- }
312- }
313-
314- /**
315- * {@link MongoQueryExecution} to return a single entity with update.
316- *
317- * @author Thomas Darimont
318- */
319- final class UpdatingSingleEntityExecution implements MongoQueryExecution {
320-
321- private final Update update ;
322-
323- private UpdatingSingleEntityExecution (Update update ) {
324- this .update = update ;
325- }
326-
327- /*
328- * (non-Javadoc)
329- * @see org.springframework.data.mongodb.repository.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.core.query.Query)
330- */
331- @ Override
332- public Object execute (Query query ) {
333-
334- MongoEntityMetadata <?> metadata = method .getEntityInformation ();
335- return operations .findAndModify (query .limit (1 ), update , metadata .getJavaType (), metadata .getCollectionName ());
336- }
337- }
338395}
0 commit comments