diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java index ca6658b3c..ea7ef1966 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java @@ -19,6 +19,7 @@ import java.util.Optional; 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.lang.Nullable; import com.mongodb.client.result.UpdateResult; @@ -151,13 +152,26 @@ public interface ExecutableUpdateOperation { interface UpdateWithUpdate { /** - * Set the {@link Update} to be applied. + * Set the {@link UpdateDefinition} to be applied. * * @param update must not be {@literal null}. * @return new instance of {@link TerminatingUpdate}. * @throws IllegalArgumentException if update is {@literal null}. */ - TerminatingUpdate apply(Update update); + TerminatingUpdate apply(UpdateDefinition update); + + /** + * Set the {@link Update} to be applied. + * + * @param update must not be {@literal null}. + * @return new instance of {@link TerminatingUpdate}. + * @throws IllegalArgumentException if update is {@literal null}. + * @deprecated since 2.3 in favor of {@link #apply(UpdateDefinition)}. + */ + @Deprecated + default TerminatingUpdate apply(Update update) { + return apply((UpdateDefinition) update); + } /** * Specify {@code replacement} object. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java index b7b5141d0..09bac18ba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java @@ -21,7 +21,7 @@ import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; 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.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -67,7 +67,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { @NonNull MongoTemplate template; @NonNull Class domainType; Query query; - @Nullable Update update; + @Nullable UpdateDefinition update; @Nullable String collection; @Nullable FindAndModifyOptions findAndModifyOptions; @Nullable FindAndReplaceOptions findAndReplaceOptions; @@ -76,10 +76,10 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithUpdate#apply(Update) + * @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithUpdate#apply(org.springframework.data.mongodb.core.query.UpdateDefinition) */ @Override - public TerminatingUpdate apply(Update update) { + public TerminatingUpdate apply(UpdateDefinition update) { Assert.notNull(update, "Update must not be null!"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 0a5cf24fa..c1d2d7c6a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -40,6 +40,7 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; 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.util.CloseableIterator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -57,7 +58,7 @@ import com.mongodb.client.result.UpdateResult; * Interface that specifies a basic set of MongoDB operations. Implemented by {@link MongoTemplate}. Not often used but * a useful option for extensibility and testability (as it can be easily mocked, stubbed, or be the target of a JDK * proxy). - *

+ *

* NOTE: Some operations cannot be executed within a MongoDB transaction. Please refer to the MongoDB * specific documentation to learn more about Multi * Document Transactions. @@ -175,7 +176,7 @@ public interface MongoOperations extends FluentMongoOperations { /** * Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession} * provided by the given {@link Supplier} to each and every command issued against MongoDB. - *

+ *

* Note: It is up to the caller to manage the {@link ClientSession} lifecycle. Use the * {@link SessionScoped#execute(SessionCallback, Consumer)} hook to potentially close the {@link ClientSession}. * @@ -211,7 +212,7 @@ public interface MongoOperations extends FluentMongoOperations { /** * Obtain a {@link ClientSession} bound instance of {@link MongoOperations}. - *

+ *

* Note: It is up to the caller to manage the {@link ClientSession} lifecycle. * * @param session must not be {@literal null}. @@ -653,7 +654,7 @@ public interface MongoOperations extends FluentMongoOperations { * {@code $geoNear} aggregation command to emulate {@code geoNear} command functionality. We recommend using * aggregations directly: *

- * + * *
 	 * TypedAggregation<T> geoNear = TypedAggregation.newAggregation(entityClass, Aggregation.geoNear(near, "dis"))
 	 * 		.withOptions(AggregationOptions.builder().collation(near.getCollation()).build());
@@ -678,7 +679,7 @@ public interface MongoOperations extends FluentMongoOperations {
 	 * {@code $geoNear} aggregation command to emulate {@code geoNear} command functionality. We recommend using
 	 * aggregations directly:
 	 * 

- * + * *
 	 * TypedAggregation<T> geoNear = TypedAggregation.newAggregation(entityClass, Aggregation.geoNear(near, "dis"))
 	 * 		.withOptions(AggregationOptions.builder().collation(near.getCollation()).build());
@@ -877,6 +878,20 @@ public interface MongoOperations extends FluentMongoOperations {
 		return findDistinct(query, field, collection, Object.class, resultClass);
 	}
 
+	/**
+	 * Triggers findAndModify 
+	 * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
+	 *
+	 * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
+	 *          fields specification. Must not be {@literal null}.
+	 * @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}.
+	 * @param entityClass the parametrized type. Must not be {@literal null}.
+	 * @return the converted object that was updated before it was updated or {@literal null}, if not found.
+	 * @since 2.3
+	 */
+	@Nullable
+	 T findAndModify(Query query, UpdateDefinition update, Class entityClass);
+
 	/**
 	 * Triggers findAndModify 
 	 * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
@@ -886,9 +901,28 @@ public interface MongoOperations extends FluentMongoOperations {
 	 * @param update the {@link Update} to apply on matching documents. Must not be {@literal null}.
 	 * @param entityClass the parametrized type. Must not be {@literal null}.
 	 * @return the converted object that was updated before it was updated or {@literal null}, if not found.
+	 * @deprecated since 2.3 in favor of {@link #findAndModify(Query, UpdateDefinition, Class)}.
+	 */
+	@Deprecated
+	@Nullable
+	default  T findAndModify(Query query, Update update, Class entityClass) {
+		return findAndModify(query, (UpdateDefinition) update, entityClass);
+	}
+
+	/**
+	 * Triggers findAndModify 
+	 * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
+	 *
+	 * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
+	 *          fields specification. Must not be {@literal null}.
+	 * @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}.
+	 * @param entityClass the parametrized type. Must not be {@literal null}.
+	 * @param collectionName the collection to query. Must not be {@literal null}.
+	 * @return the converted object that was updated before it was updated or {@literal null}, if not found.
+	 * @since 2.3
 	 */
 	@Nullable
-	 T findAndModify(Query query, Update update, Class entityClass);
+	 T findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName);
 
 	/**
 	 * Triggers findAndModify 
@@ -900,9 +934,31 @@ public interface MongoOperations extends FluentMongoOperations {
 	 * @param entityClass the parametrized type. Must not be {@literal null}.
 	 * @param collectionName the collection to query. Must not be {@literal null}.
 	 * @return the converted object that was updated before it was updated or {@literal null}, if not found.
+	 * @deprecated since 2.3 in favor of {@link #findAndModify(Query, UpdateDefinition, Class, String)}.
+	 */
+	@Deprecated
+	@Nullable
+	default  T findAndModify(Query query, Update update, Class entityClass, String collectionName) {
+		return findAndModify(query, (UpdateDefinition) update, entityClass, collectionName);
+	}
+
+	/**
+	 * Triggers findAndModify 
+	 * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
+	 * {@link FindAndModifyOptions} into account.
+	 *
+	 * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
+	 *          fields specification.
+	 * @param update the {@link UpdateDefinition} to apply on matching documents.
+	 * @param options the {@link FindAndModifyOptions} holding additional information.
+	 * @param entityClass the parametrized type.
+	 * @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
+	 *         {@link FindAndModifyOptions#isReturnNew()} this will either be the object as it was before the update or as
+	 *         it is after the update.
+	 * @since 2.3
 	 */
 	@Nullable
-	 T findAndModify(Query query, Update update, Class entityClass, String collectionName);
+	 T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass);
 
 	/**
 	 * Triggers findAndModify 
@@ -917,9 +973,33 @@ public interface MongoOperations extends FluentMongoOperations {
 	 * @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
 	 *         {@link FindAndModifyOptions#isReturnNew()} this will either be the object as it was before the update or as
 	 *         it is after the update.
+	 * @deprecated since 2.3 in favor of {@link #findAndModify(Query, UpdateDefinition, FindAndModifyOptions, Class)}
 	 */
 	@Nullable
-	 T findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass);
+	@Deprecated
+	default  T findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass) {
+		return findAndModify(query, (UpdateDefinition) update, options, entityClass);
+	}
+
+	/**
+	 * Triggers findAndModify 
+	 * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
+	 * {@link FindAndModifyOptions} into account.
+	 *
+	 * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
+	 *          fields specification. Must not be {@literal null}.
+	 * @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}.
+	 * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
+	 * @param entityClass the parametrized type. Must not be {@literal null}.
+	 * @param collectionName the collection to query. Must not be {@literal null}.
+	 * @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
+	 *         {@link FindAndModifyOptions#isReturnNew()} this will either be the object as it was before the update or as
+	 *         it is after the update.
+	 * @since 2.3
+	 */
+	@Nullable
+	 T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass,
+			String collectionName);
 
 	/**
 	 * Triggers findAndModify 
@@ -935,10 +1015,15 @@ public interface MongoOperations extends FluentMongoOperations {
 	 * @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
 	 *         {@link FindAndModifyOptions#isReturnNew()} this will either be the object as it was before the update or as
 	 *         it is after the update.
+	 * @deprecated since 2.3 in favor of
+	 *             {@link #findAndModify(Query, UpdateDefinition, FindAndModifyOptions, Class, String)}.
 	 */
+	@Deprecated
 	@Nullable
-	 T findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass,
-			String collectionName);
+	default  T findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass,
+			String collectionName) {
+		return findAndModify(query, (UpdateDefinition) update, options, entityClass, collectionName);
+	}
 
 	/**
 	 * Triggers
@@ -1294,8 +1379,26 @@ public interface MongoOperations extends FluentMongoOperations {
 	 *          object. Must not be {@literal null}.
 	 * @param entityClass class that determines the collection to use. Must not be {@literal null}.
 	 * @return the {@link UpdateResult} which lets you access the results of the previous write.
+	 * @since 2.3
 	 */
-	UpdateResult upsert(Query query, Update update, Class entityClass);
+	UpdateResult upsert(Query query, UpdateDefinition update, Class entityClass);
+
+	/**
+	 * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by
+	 * combining the query document and the update document.
+	 *
+	 * @param query the query document that specifies the criteria used to select a record to be upserted. Must not be
+	 *          {@literal null}.
+	 * @param update the update document that contains the updated object or $ operators to manipulate the existing
+	 *          object. Must not be {@literal null}.
+	 * @param entityClass class that determines the collection to use. Must not be {@literal null}.
+	 * @return the {@link UpdateResult} which lets you access the results of the previous write.
+	 * @deprecated since 2.3 in favor of {@link #upsert(Query, UpdateDefinition, Class)}
+	 */
+	@Deprecated
+	default UpdateResult upsert(Query query, Update update, Class entityClass) {
+		return upsert(query, (UpdateDefinition) update, entityClass);
+	}
 
 	/**
 	 * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by
@@ -1312,8 +1415,43 @@ public interface MongoOperations extends FluentMongoOperations {
 	 *          object. Must not be {@literal null}.
 	 * @param collectionName name of the collection to update the object in.
 	 * @return the {@link UpdateResult} which lets you access the results of the previous write.
+	 * @since 2.3
 	 */
-	UpdateResult upsert(Query query, Update update, String collectionName);
+	UpdateResult upsert(Query query, UpdateDefinition update, String collectionName);
+
+	/**
+	 * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by
+	 * combining the query document and the update document. 
+ * NOTE: Any additional support for field mapping, versions, etc. is not available due to the lack of + * domain type information. Use {@link #upsert(Query, Update, Class, String)} to get full type specific support. + * + * @param query the query document that specifies the criteria used to select a record to be upserted. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing + * object. Must not be {@literal null}. + * @param collectionName name of the collection to update the object in. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #upsert(Query, UpdateDefinition, String)} + */ + @Deprecated + default UpdateResult upsert(Query query, Update update, String collectionName) { + return upsert(query, (UpdateDefinition) update, collectionName); + } + + /** + * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by + * combining the query document and the update document. + * + * @param query the query document that specifies the criteria used to select a record to be upserted. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing + * object. Must not be {@literal null}. + * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 + */ + UpdateResult upsert(Query query, UpdateDefinition update, Class entityClass, String collectionName); /** * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by @@ -1328,8 +1466,26 @@ public interface MongoOperations extends FluentMongoOperations { * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #upsert(Query, UpdateDefinition, Class, String)} */ - UpdateResult upsert(Query query, Update update, Class entityClass, String collectionName); + @Deprecated + default UpdateResult upsert(Query query, Update update, Class entityClass, String collectionName) { + return upsert(query, (UpdateDefinition) update, entityClass, collectionName); + } + + /** + * Updates the first object that is found in the collection of the entity class that matches the query document with + * the provided update document. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param entityClass class that determines the collection to use. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 + */ + UpdateResult updateFirst(Query query, UpdateDefinition update, Class entityClass); /** * Updates the first object that is found in the collection of the entity class that matches the query document with @@ -1343,8 +1499,12 @@ public interface MongoOperations extends FluentMongoOperations { * not be {@literal null}. * @param entityClass class that determines the collection to use. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateFirst(Query, UpdateDefinition, Class)}. */ - UpdateResult updateFirst(Query query, Update update, Class entityClass); + @Deprecated + default UpdateResult updateFirst(Query query, Update update, Class entityClass) { + return updateFirst(query, (UpdateDefinition) update, entityClass); + } /** * Updates the first object that is found in the specified collection that matches the query document criteria with @@ -1361,8 +1521,43 @@ public interface MongoOperations extends FluentMongoOperations { * not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - UpdateResult updateFirst(Query query, Update update, String collectionName); + UpdateResult updateFirst(Query query, UpdateDefinition update, String collectionName); + + /** + * Updates the first object that is found in the specified collection that matches the query document criteria with + * the provided updated document.
+ * NOTE: Any additional support for field mapping, versions, etc. is not available due to the lack of + * domain type information. Use {@link #updateFirst(Query, Update, Class, String)} to get full type specific support. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateFirst(Query, UpdateDefinition, String)}. + */ + @Deprecated + default UpdateResult updateFirst(Query query, Update update, String collectionName) { + return updateFirst(query, (UpdateDefinition) update, collectionName); + } + + /** + * Updates the first object that is found in the specified collection that matches the query document criteria with + * the provided updated document.
+ * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 + */ + UpdateResult updateFirst(Query query, UpdateDefinition update, Class entityClass, String collectionName); /** * Updates the first object that is found in the specified collection that matches the query document criteria with @@ -1377,8 +1572,12 @@ public interface MongoOperations extends FluentMongoOperations { * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateFirst(Query, UpdateDefinition, Class, String)}. */ - UpdateResult updateFirst(Query query, Update update, Class entityClass, String collectionName); + @Deprecated + default UpdateResult updateFirst(Query query, Update update, Class entityClass, String collectionName) { + return updateFirst(query, (UpdateDefinition) update, entityClass, collectionName); + } /** * Updates all objects that are found in the collection for the entity class that matches the query document criteria @@ -1390,8 +1589,26 @@ public interface MongoOperations extends FluentMongoOperations { * not be {@literal null}. * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - UpdateResult updateMulti(Query query, Update update, Class entityClass); + UpdateResult updateMulti(Query query, UpdateDefinition update, Class entityClass); + + /** + * Updates all objects that are found in the collection for the entity class that matches the query document criteria + * with the provided updated document. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateMulti(Query, UpdateDefinition, Class)}. + */ + @Deprecated + default UpdateResult updateMulti(Query query, Update update, Class entityClass) { + return updateMulti(query, (UpdateDefinition) update, entityClass); + } /** * Updates all objects that are found in the specified collection that matches the query document criteria with the @@ -1405,8 +1622,28 @@ public interface MongoOperations extends FluentMongoOperations { * not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - UpdateResult updateMulti(Query query, Update update, String collectionName); + UpdateResult updateMulti(Query query, UpdateDefinition update, String collectionName); + + /** + * Updates all objects that are found in the specified collection that matches the query document criteria with the + * provided updated document.
+ * NOTE: Any additional support for field mapping, versions, etc. is not available due to the lack of + * domain type information. Use {@link #updateMulti(Query, Update, Class, String)} to get full type specific support. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateMulti(Query, UpdateDefinition, String)}. + */ + @Deprecated + default UpdateResult updateMulti(Query query, Update update, String collectionName) { + return updateMulti(query, (UpdateDefinition) update, collectionName); + } /** * Updates all objects that are found in the collection for the entity class that matches the query document criteria @@ -1419,8 +1656,27 @@ public interface MongoOperations extends FluentMongoOperations { * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - UpdateResult updateMulti(Query query, Update update, Class entityClass, String collectionName); + UpdateResult updateMulti(Query query, UpdateDefinition update, Class entityClass, String collectionName); + + /** + * Updates all objects that are found in the collection for the entity class that matches the query document criteria + * with the provided updated document. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateMulti(Query, UpdateDefinition, Class, String)}. + */ + @Deprecated + default UpdateResult updateMulti(Query query, Update update, Class entityClass, String collectionName) { + return updateMulti(query, (UpdateDefinition) update, entityClass, collectionName); + } /** * Remove the given object from the collection by {@literal id} and (if applicable) its diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 9c3b0d6b5..a2f2a804e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -69,7 +69,9 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.aggregation.Fields; +import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.convert.DbRefResolver; @@ -109,7 +111,6 @@ import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Meta.CursorOption; import org.springframework.data.mongodb.core.query.NearQuery; 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.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.mongodb.core.validation.Validator; @@ -1046,25 +1047,25 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable @Override - public T findAndModify(Query query, Update update, Class entityClass) { + public T findAndModify(Query query, UpdateDefinition update, Class entityClass) { return findAndModify(query, update, new FindAndModifyOptions(), entityClass, getCollectionName(entityClass)); } @Nullable @Override - public T findAndModify(Query query, Update update, Class entityClass, String collectionName) { + public T findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName) { return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName); } @Nullable @Override - public T findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass) { + public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass) { return findAndModify(query, update, options, entityClass, getCollectionName(entityClass)); } @Nullable @Override - public T findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass, + public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, String collectionName) { Assert.notNull(query, "Query must not be null!"); @@ -1564,17 +1565,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } @Override - public UpdateResult upsert(Query query, Update update, Class entityClass) { + public UpdateResult upsert(Query query, UpdateDefinition update, Class entityClass) { return doUpdate(getCollectionName(entityClass), query, update, entityClass, true, false); } @Override - public UpdateResult upsert(Query query, Update update, String collectionName) { + public UpdateResult upsert(Query query, UpdateDefinition update, String collectionName) { return doUpdate(collectionName, query, update, null, true, false); } @Override - public UpdateResult upsert(Query query, Update update, Class entityClass, String collectionName) { + public UpdateResult upsert(Query query, UpdateDefinition update, Class entityClass, String collectionName) { Assert.notNull(entityClass, "EntityClass must not be null!"); @@ -1582,17 +1583,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } @Override - public UpdateResult updateFirst(Query query, Update update, Class entityClass) { + public UpdateResult updateFirst(Query query, UpdateDefinition update, Class entityClass) { return doUpdate(getCollectionName(entityClass), query, update, entityClass, false, false); } @Override - public UpdateResult updateFirst(final Query query, final Update update, final String collectionName) { + public UpdateResult updateFirst(final Query query, final UpdateDefinition update, final String collectionName) { return doUpdate(collectionName, query, update, null, false, false); } @Override - public UpdateResult updateFirst(Query query, Update update, Class entityClass, String collectionName) { + public UpdateResult updateFirst(Query query, UpdateDefinition update, Class entityClass, String collectionName) { Assert.notNull(entityClass, "EntityClass must not be null!"); @@ -1600,17 +1601,18 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } @Override - public UpdateResult updateMulti(Query query, Update update, Class entityClass) { + public UpdateResult updateMulti(Query query, UpdateDefinition update, Class entityClass) { return doUpdate(getCollectionName(entityClass), query, update, entityClass, false, true); } @Override - public UpdateResult updateMulti(final Query query, final Update update, String collectionName) { + public UpdateResult updateMulti(final Query query, final UpdateDefinition update, String collectionName) { return doUpdate(collectionName, query, update, null, false, true); } @Override - public UpdateResult updateMulti(final Query query, final Update update, Class entityClass, String collectionName) { + public UpdateResult updateMulti(final Query query, final UpdateDefinition update, Class entityClass, + String collectionName) { Assert.notNull(entityClass, "EntityClass must not be null!"); @@ -1631,25 +1633,53 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, upsert ? "Upsert" : "UpdateFirst", serializeToJsonSafely(query.getSortObject())); } + MongoPersistentEntity entity = entityClass == null ? null : getPersistentEntity(entityClass); + increaseVersionForUpdateIfNecessary(entity, update); + + UpdateOptions opts = new UpdateOptions(); + opts.upsert(upsert); + + if (update.hasArrayFilters()) { + opts.arrayFilters(update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList())); + } + + Document queryObj = new Document(); + + if (query != null) { + queryObj.putAll(queryMapper.getMappedObject(query.getQueryObject(), entity)); + } + + if (multi && update.isIsolated() && !queryObj.containsKey("$isolated")) { + queryObj.put("$isolated", 1); + } + + if (update instanceof AggregationUpdate) { + + AggregationOperationContext context = entityClass != null + ? new RelaxedTypeBasedAggregationOperationContext(entityClass, mappingContext, queryMapper) + : Aggregation.DEFAULT_CONTEXT; + + AggregationUpdate aUppdate = ((AggregationUpdate) update); + List pipeline = new AggregationUtil(queryMapper, mappingContext).createPipeline(aUppdate, context); + + return execute(collectionName, collection -> { + + MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName, + entityClass, update.getUpdateObject(), queryObj); + WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + + collection = writeConcernToUse != null ? collection.withWriteConcern(writeConcernToUse) : collection; + + if (multi) { + return collection.updateMany(queryObj, pipeline, opts); + } + + return collection.updateOne(queryObj, pipeline, opts); + }); + } + return execute(collectionName, collection -> { - MongoPersistentEntity entity = entityClass == null ? null : getPersistentEntity(entityClass); - - increaseVersionForUpdateIfNecessary(entity, update); - - UpdateOptions opts = new UpdateOptions(); - opts.upsert(upsert); - - if (update.hasArrayFilters()) { - opts.arrayFilters(update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList())); - } - - Document queryObj = new Document(); - - if (query != null) { - queryObj.putAll(queryMapper.getMappedObject(query.getQueryObject(), entity)); - } - operations.forType(entityClass) // .getCollation(query) // .map(Collation::toMongoCollation) // @@ -1658,10 +1688,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document updateObj = update instanceof MappedUpdate ? update.getUpdateObject() : updateMapper.getMappedObject(update.getUpdateObject(), entity); - if (multi && update.isIsolated() && !queryObj.containsKey("$isolated")) { - queryObj.put("$isolated", 1); - } - if (LOGGER.isDebugEnabled()) { LOGGER.debug("Calling update using query: {} and update: {} in collection: {}", serializeToJsonSafely(queryObj), serializeToJsonSafely(updateObj), collectionName); @@ -2653,7 +2679,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @SuppressWarnings("ConstantConditions") protected T doFindAndModify(String collectionName, Document query, Document fields, Document sort, - Class entityClass, Update update, @Nullable FindAndModifyOptions options) { + Class entityClass, UpdateDefinition update, @Nullable FindAndModifyOptions options) { EntityReader readerToUse = this.mongoConverter; @@ -2666,7 +2692,18 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, increaseVersionForUpdateIfNecessary(entity, update); Document mappedQuery = queryMapper.getMappedObject(query, entity); - Document mappedUpdate = updateMapper.getMappedObject(update.getUpdateObject(), entity); + + Object mappedUpdate = new Document(); + if (update instanceof AggregationUpdate) { + + AggregationOperationContext context = entityClass != null + ? new RelaxedTypeBasedAggregationOperationContext(entityClass, mappingContext, queryMapper) + : Aggregation.DEFAULT_CONTEXT; + + mappedUpdate = new AggregationUtil(queryMapper, mappingContext).createPipeline((Aggregation) update, context); + } else { + mappedUpdate = updateMapper.getMappedObject(update.getUpdateObject(), entity); + } if (LOGGER.isDebugEnabled()) { LOGGER.debug( @@ -3040,11 +3077,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final Document query; private final Document fields; private final Document sort; - private final Document update; + private final Object update; private final List arrayFilters; private final FindAndModifyOptions options; - public FindAndModifyCallback(Document query, Document fields, Document sort, Document update, + public FindAndModifyCallback(Document query, Document fields, Document sort, Object update, List arrayFilters, FindAndModifyOptions options) { this.query = query; this.fields = fields; @@ -3072,7 +3109,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, opts.arrayFilters(arrayFilters); } - return collection.findOneAndUpdate(query, update, opts); + if (update instanceof Document) { + return collection.findOneAndUpdate(query, (Document) update, opts); + } else if (update instanceof List) { + return collection.findOneAndUpdate(query, (List) update, opts); + } + throw new IllegalArgumentException("doh - that does not work"); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java index 9d68fca4a..93bde21ee 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java @@ -39,6 +39,7 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; 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.lang.Nullable; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.Assert; @@ -672,6 +673,19 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { @Deprecated Flux> geoNear(NearQuery near, Class entityClass, String collectionName); + /** + * Triggers
findAndModify + * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. Must not be {@literal null}. + * @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}. + * @param entityClass the parametrized type. Must not be {@literal null}. + * @return the converted object that was updated before it was updated. + * @since 2.3 + */ + Mono findAndModify(Query query, UpdateDefinition update, Class entityClass); + /** * Triggers findAndModify * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}. @@ -681,8 +695,26 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @param update the {@link Update} to apply on matching documents. Must not be {@literal null}. * @param entityClass the parametrized type. Must not be {@literal null}. * @return the converted object that was updated before it was updated. + * @deprecated since 2.3 in favor of {@link #findAndModify(Query, UpdateDefinition, Class)}. */ - Mono findAndModify(Query query, Update update, Class entityClass); + @Deprecated + default Mono findAndModify(Query query, Update update, Class entityClass) { + return findAndModify(query, (UpdateDefinition) update, entityClass); + } + + /** + * Triggers findAndModify + * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. Must not be {@literal null}. + * @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}. + * @param entityClass the parametrized type. Must not be {@literal null}. + * @param collectionName the collection to query. Must not be {@literal null}. + * @return the converted object that was updated before it was updated. + * @since 2.3 + */ + Mono findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName); /** * Triggers findAndModify @@ -694,8 +726,28 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @param entityClass the parametrized type. Must not be {@literal null}. * @param collectionName the collection to query. Must not be {@literal null}. * @return the converted object that was updated before it was updated. + * @deprecated since 2.3 in favor of {@link #findAndModify(Query, UpdateDefinition, Class, String)}. */ - Mono findAndModify(Query query, Update update, Class entityClass, String collectionName); + @Deprecated + default Mono findAndModify(Query query, Update update, Class entityClass, String collectionName) { + return findAndModify(query, (UpdateDefinition) update, entityClass, collectionName); + } + + /** + * Triggers findAndModify + * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking + * {@link FindAndModifyOptions} into account. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. + * @param update the {@link UpdateDefinition} to apply on matching documents. + * @param options the {@link FindAndModifyOptions} holding additional information. + * @param entityClass the parametrized type. + * @return the converted object that was updated. Depending on the value of {@link FindAndModifyOptions#isReturnNew()} + * this will either be the object as it was before the update or as it is after the update. + * @since 2.3 + */ + Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass); /** * Triggers findAndModify @@ -709,8 +761,30 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @param entityClass the parametrized type. * @return the converted object that was updated. Depending on the value of {@link FindAndModifyOptions#isReturnNew()} * this will either be the object as it was before the update or as it is after the update. + * @deprecated since 2.3 in favor of {@link #findAndModify(Query, UpdateDefinition, FindAndModifyOptions, Class)}. */ - Mono findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass); + @Deprecated + default Mono findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass) { + return findAndModify(query, (UpdateDefinition) update, options, entityClass); + } + + /** + * Triggers findAndModify + * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking + * {@link FindAndModifyOptions} into account. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. Must not be {@literal null}. + * @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}. + * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}. + * @param entityClass the parametrized type. Must not be {@literal null}. + * @param collectionName the collection to query. Must not be {@literal null}. + * @return the converted object that was updated. Depending on the value of {@link FindAndModifyOptions#isReturnNew()} + * this will either be the object as it was before the update or as it is after the update. + * @since 2.3 + */ + Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, + String collectionName); /** * Triggers findAndModify @@ -725,9 +799,14 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @param collectionName the collection to query. Must not be {@literal null}. * @return the converted object that was updated. Depending on the value of {@link FindAndModifyOptions#isReturnNew()} * this will either be the object as it was before the update or as it is after the update. + * @deprecated since 2.3 in favor of + * {@link #findAndModify(Query, UpdateDefinition, FindAndModifyOptions, Class, String)}. */ - Mono findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass, - String collectionName); + @Deprecated + default Mono findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass, + String collectionName) { + return findAndModify(query, (UpdateDefinition) update, options, entityClass, collectionName); + } /** * Triggers @@ -1157,8 +1236,42 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * object. Must not be {@literal null}. * @param entityClass class that determines the collection to use. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - Mono upsert(Query query, Update update, Class entityClass); + Mono upsert(Query query, UpdateDefinition update, Class entityClass); + + /** + * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by + * combining the query document and the update document. + * + * @param query the query document that specifies the criteria used to select a record to be upserted. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing + * object. Must not be {@literal null}. + * @param entityClass class that determines the collection to use. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #upsert(Query, UpdateDefinition, Class)}. + */ + @Deprecated + default Mono upsert(Query query, Update update, Class entityClass) { + return upsert(query, (UpdateDefinition) update, entityClass); + } + + /** + * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by + * combining the query document and the update document.
+ * NOTE: Any additional support for field mapping, versions, etc. is not available due to the lack of + * domain type information. Use {@link #upsert(Query, Update, Class, String)} to get full type specific support. + * + * @param query the query document that specifies the criteria used to select a record to be upserted. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing + * object. Must not be {@literal null}. + * @param collectionName name of the collection to update the object in. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 + */ + Mono upsert(Query query, UpdateDefinition update, String collectionName); /** * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by @@ -1175,8 +1288,27 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * object. Must not be {@literal null}. * @param collectionName name of the collection to update the object in. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #upsert(Query, UpdateDefinition, String)}. */ - Mono upsert(Query query, Update update, String collectionName); + @Deprecated + default Mono upsert(Query query, Update update, String collectionName) { + return upsert(query, (UpdateDefinition) update, collectionName); + } + + /** + * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by + * combining the query document and the update document. + * + * @param query the query document that specifies the criteria used to select a record to be upserted. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing + * object. Must not be {@literal null}. + * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 + */ + Mono upsert(Query query, UpdateDefinition update, Class entityClass, String collectionName); /** * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by @@ -1191,8 +1323,12 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #upsert(Query, UpdateDefinition, Class, String)}. */ - Mono upsert(Query query, Update update, Class entityClass, String collectionName); + @Deprecated + default Mono upsert(Query query, Update update, Class entityClass, String collectionName) { + return upsert(query, (UpdateDefinition) update, entityClass, collectionName); + } /** * Updates the first object that is found in the collection of the entity class that matches the query document with @@ -1206,8 +1342,26 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * not be {@literal null}. * @param entityClass class that determines the collection to use. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - Mono updateFirst(Query query, Update update, Class entityClass); + Mono updateFirst(Query query, UpdateDefinition update, Class entityClass); + + /** + * Updates the first object that is found in the collection of the entity class that matches the query document with + * the provided update document. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param entityClass class that determines the collection to use. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateFirst(Query, UpdateDefinition, Class)}. + */ + @Deprecated + default Mono updateFirst(Query query, Update update, Class entityClass) { + return updateFirst(query, (UpdateDefinition) update, entityClass); + } /** * Updates the first object that is found in the specified collection that matches the query document criteria with @@ -1224,8 +1378,28 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - Mono updateFirst(Query query, Update update, String collectionName); + Mono updateFirst(Query query, UpdateDefinition update, String collectionName); + + /** + * Updates the first object that is found in the specified collection that matches the query document criteria with + * the provided updated document.
+ * NOTE: Any additional support for field mapping, versions, etc. is not available due to the lack of + * domain type information. Use {@link #updateFirst(Query, Update, Class, String)} to get full type specific support. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateFirst(Query, UpdateDefinition, String)}. + */ + @Deprecated + default Mono updateFirst(Query query, Update update, String collectionName) { + return updateFirst(query, (UpdateDefinition) update, collectionName); + } /** * Updates the first object that is found in the specified collection that matches the query document criteria with @@ -1240,8 +1414,27 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - Mono updateFirst(Query query, Update update, Class entityClass, String collectionName); + Mono updateFirst(Query query, UpdateDefinition update, Class entityClass, String collectionName); + + /** + * Updates the first object that is found in the specified collection that matches the query document criteria with + * the provided updated document.
+ * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateFirst(Query, UpdateDefinition, Class, String)}. + */ + @Deprecated + default Mono updateFirst(Query query, Update update, Class entityClass, String collectionName) { + return updateFirst(query, (UpdateDefinition) update, entityClass, collectionName); + } /** * Updates all objects that are found in the collection for the entity class that matches the query document criteria @@ -1253,8 +1446,26 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * not be {@literal null}. * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - Mono updateMulti(Query query, Update update, Class entityClass); + Mono updateMulti(Query query, UpdateDefinition update, Class entityClass); + + /** + * Updates all objects that are found in the collection for the entity class that matches the query document criteria + * with the provided updated document. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateMulti(Query, UpdateDefinition, Class)}. + */ + @Deprecated + default Mono updateMulti(Query query, Update update, Class entityClass) { + return updateMulti(query, (UpdateDefinition) update, entityClass); + } /** * Updates all objects that are found in the specified collection that matches the query document criteria with the @@ -1268,8 +1479,28 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - Mono updateMulti(Query query, Update update, String collectionName); + Mono updateMulti(Query query, UpdateDefinition update, String collectionName); + + /** + * Updates all objects that are found in the specified collection that matches the query document criteria with the + * provided updated document.
+ * NOTE: Any additional support for field mapping, versions, etc. is not available due to the lack of + * domain type information. Use {@link #updateMulti(Query, Update, Class, String)} to get full type specific support. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateMulti(Query, UpdateDefinition, String)}. + */ + @Deprecated + default Mono updateMulti(Query query, Update update, String collectionName) { + return updateMulti(query, (UpdateDefinition) update, collectionName); + } /** * Updates all objects that are found in the collection for the entity class that matches the query document criteria @@ -1282,8 +1513,27 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @since 2.3 */ - Mono updateMulti(Query query, Update update, Class entityClass, String collectionName); + Mono updateMulti(Query query, UpdateDefinition update, Class entityClass, String collectionName); + + /** + * Updates all objects that are found in the collection for the entity class that matches the query document criteria + * with the provided updated document. + * + * @param query the query document that specifies the criteria used to select a record to be updated. Must not be + * {@literal null}. + * @param update the update document that contains the updated object or $ operators to manipulate the existing. Must + * not be {@literal null}. + * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. + * @param collectionName name of the collection to update the object in. Must not be {@literal null}. + * @return the {@link UpdateResult} which lets you access the results of the previous write. + * @deprecated since 2.3 in favor of {@link #updateMulti(Query, UpdateDefinition, Class, String)}. + */ + @Deprecated + default Mono updateMulti(Query query, Update update, Class entityClass, String collectionName) { + return updateMulti(query, (UpdateDefinition) update, entityClass, collectionName); + } /** * Remove the given object from the collection by id. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index a8944f347..39c83c6ee 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -69,7 +69,9 @@ import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.aggregation.PrefixingDelegatingAggregationOperationContext; +import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.convert.DbRefResolver; @@ -105,7 +107,6 @@ import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Meta.CursorOption; import org.springframework.data.mongodb.core.query.NearQuery; 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.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.mongodb.core.validation.Validator; @@ -1125,34 +1126,35 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndModify(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.Class) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndModify(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.Class) */ - public Mono findAndModify(Query query, Update update, Class entityClass) { + public Mono findAndModify(Query query, UpdateDefinition update, Class entityClass) { return findAndModify(query, update, new FindAndModifyOptions(), entityClass, getCollectionName(entityClass)); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndModify(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.Class, java.lang.String) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndModify(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.Class, java.lang.String) */ - public Mono findAndModify(Query query, Update update, Class entityClass, String collectionName) { + public Mono findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName) { return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndModify(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, org.springframework.data.mongodb.core.FindAndModifyOptions, java.lang.Class) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndModify(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, org.springframework.data.mongodb.core.FindAndModifyOptions, java.lang.Class) */ - public Mono findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass) { + public Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass) { return findAndModify(query, update, options, entityClass, getCollectionName(entityClass)); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndModify(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, org.springframework.data.mongodb.core.FindAndModifyOptions, java.lang.Class, java.lang.String) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndModify(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, org.springframework.data.mongodb.core.FindAndModifyOptions, java.lang.Class, java.lang.String) */ - public Mono findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass, - String collectionName) { + public Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName) { Assert.notNull(options, "Options must not be null! "); @@ -1165,7 +1167,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati if (!optionsToUse.getCollation().isPresent()) { operations.forType(entityClass).getCollation(query).ifPresent(optionsToUse::collation); - ; } return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(), @@ -1687,73 +1688,75 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#upsert(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.Class) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#upsert(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.Class) */ - public Mono upsert(Query query, Update update, Class entityClass) { + public Mono upsert(Query query, UpdateDefinition update, Class entityClass) { return doUpdate(getCollectionName(entityClass), query, update, entityClass, true, false); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#upsert(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.String) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#upsert(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.String) */ - public Mono upsert(Query query, Update update, String collectionName) { + public Mono upsert(Query query, UpdateDefinition update, String collectionName) { return doUpdate(collectionName, query, update, null, true, false); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#upsert(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.Class, java.lang.String) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#upsert(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.Class, java.lang.String) */ - public Mono upsert(Query query, Update update, Class entityClass, String collectionName) { + public Mono upsert(Query query, UpdateDefinition update, Class entityClass, String collectionName) { return doUpdate(collectionName, query, update, entityClass, true, false); } /* * (non-Javadoc)) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateFirst(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.Class) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateFirst(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.Class) */ - public Mono updateFirst(Query query, Update update, Class entityClass) { + public Mono updateFirst(Query query, UpdateDefinition update, Class entityClass) { return doUpdate(getCollectionName(entityClass), query, update, entityClass, false, false); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateFirst(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.String) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateFirst(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.String) */ - public Mono updateFirst(Query query, Update update, String collectionName) { + public Mono updateFirst(Query query, UpdateDefinition update, String collectionName) { return doUpdate(collectionName, query, update, null, false, false); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateFirst(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.Class, java.lang.String) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateFirst(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.Class, java.lang.String) */ - public Mono updateFirst(Query query, Update update, Class entityClass, String collectionName) { + public Mono updateFirst(Query query, UpdateDefinition update, Class entityClass, + String collectionName) { return doUpdate(collectionName, query, update, entityClass, false, false); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateMulti(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.Class) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateMulti(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.Class) */ - public Mono updateMulti(Query query, Update update, Class entityClass) { + public Mono updateMulti(Query query, UpdateDefinition update, Class entityClass) { return doUpdate(getCollectionName(entityClass), query, update, entityClass, false, true); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateMulti(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.String) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateMulti(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.String) */ - public Mono updateMulti(Query query, Update update, String collectionName) { + public Mono updateMulti(Query query, UpdateDefinition update, String collectionName) { return doUpdate(collectionName, query, update, null, false, true); } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateMulti(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.Update, java.lang.Class, java.lang.String) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#updateMulti(org.springframework.data.mongodb.core.query.Query, org.springframework.data.mongodb.core.query.UpdateDefinition, java.lang.Class, java.lang.String) */ - public Mono updateMulti(Query query, Update update, Class entityClass, String collectionName) { + public Mono updateMulti(Query query, UpdateDefinition update, Class entityClass, + String collectionName) { return doUpdate(collectionName, query, update, entityClass, false, true); } @@ -1767,54 +1770,86 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } MongoPersistentEntity entity = entityClass == null ? null : getPersistentEntity(entityClass); + increaseVersionForUpdateIfNecessary(entity, update); - Flux result = execute(collectionName, collection -> { + Document queryObj = queryMapper.getMappedObject(query.getQueryObject(), entity); - increaseVersionForUpdateIfNecessary(entity, update); + UpdateOptions updateOptions = new UpdateOptions().upsert(upsert); + operations.forType(entityClass).getCollation(query) // + .map(Collation::toMongoCollation) // + .ifPresent(updateOptions::collation); - Document queryObj = queryMapper.getMappedObject(query.getQueryObject(), entity); - Document updateObj = update == null ? new Document() - : updateMapper.getMappedObject(update.getUpdateObject(), entity); + if (update.hasArrayFilters()) { + updateOptions.arrayFilters(update.getArrayFilters().stream().map(ArrayFilter::asDocument) + .map(it -> queryMapper.getMappedObject(it, entity)).collect(Collectors.toList())); + } - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("Calling update using query: %s and update: %s in collection: %s", - serializeToJsonSafely(queryObj), serializeToJsonSafely(updateObj), collectionName)); - } + if (multi && update.isIsolated() && !queryObj.containsKey("$isolated")) { + queryObj.put("$isolated", 1); + } - MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName, entityClass, - updateObj, queryObj); - WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - MongoCollection collectionToUse = prepareCollection(collection, writeConcernToUse); + Flux result = Flux.empty(); - UpdateOptions updateOptions = new UpdateOptions().upsert(upsert); - operations.forType(entityClass).getCollation(query) // - .map(Collation::toMongoCollation) // - .ifPresent(updateOptions::collation); + if (update instanceof AggregationUpdate) { - if (update.hasArrayFilters()) { - updateOptions.arrayFilters(update.getArrayFilters().stream().map(ArrayFilter::asDocument) - .map(it -> queryMapper.getMappedObject(it, entity)).collect(Collectors.toList())); - } + AggregationOperationContext context = entityClass != null + ? new RelaxedTypeBasedAggregationOperationContext(entityClass, mappingContext, queryMapper) + : Aggregation.DEFAULT_CONTEXT; - if (!UpdateMapper.isUpdateObject(updateObj)) { + AggregationUpdate aUppdate = ((AggregationUpdate) update); + List pipeline = new AggregationUtil(queryMapper, mappingContext).createPipeline(aUppdate, context); - ReplaceOptions replaceOptions = new ReplaceOptions(); - replaceOptions.upsert(updateOptions.isUpsert()); - replaceOptions.collation(updateOptions.getCollation()); + result = execute(collectionName, collection -> { - return collectionToUse.replaceOne(queryObj, updateObj, replaceOptions); - } - if (multi) { - return collectionToUse.updateMany(queryObj, updateObj, updateOptions); - } - return collectionToUse.updateOne(queryObj, updateObj, updateOptions); - }).doOnNext(updateResult -> { + MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName, + entityClass, update.getUpdateObject(), queryObj); + WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + + collection = writeConcernToUse != null ? collection.withWriteConcern(writeConcernToUse) : collection; + + if (multi) { + return collection.updateMany(queryObj, pipeline, updateOptions); + } + + return collection.updateOne(queryObj, pipeline, updateOptions); + }); + } else { + + result = execute(collectionName, collection -> { + + Document updateObj = update == null ? new Document() + : updateMapper.getMappedObject(update.getUpdateObject(), entity); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("Calling update using query: %s and update: %s in collection: %s", + serializeToJsonSafely(queryObj), serializeToJsonSafely(updateObj), collectionName)); + } + + MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName, + entityClass, updateObj, queryObj); + WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + MongoCollection collectionToUse = prepareCollection(collection, writeConcernToUse); + + if (!UpdateMapper.isUpdateObject(updateObj)) { + + ReplaceOptions replaceOptions = new ReplaceOptions(); + replaceOptions.upsert(updateOptions.isUpsert()); + replaceOptions.collation(updateOptions.getCollation()); + + return collectionToUse.replaceOne(queryObj, updateObj, replaceOptions); + } + if (multi) { + return collectionToUse.updateMany(queryObj, updateObj, updateOptions); + } + return collectionToUse.updateOne(queryObj, updateObj, updateOptions); + }); + } + + result = result.doOnNext(updateResult -> { if (entity != null && entity.hasVersionProperty() && !multi) { if (updateResult.wasAcknowledged() && updateResult.getMatchedCount() == 0) { - Document queryObj = query == null ? new Document() - : queryMapper.getMappedObject(query.getQueryObject(), entity); Document updateObj = update == null ? new Document() : updateMapper.getMappedObject(update.getUpdateObject(), entity); if (containsVersionProperty(queryObj, entity)) @@ -2542,16 +2577,26 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } protected Mono doFindAndModify(String collectionName, Document query, Document fields, Document sort, - Class entityClass, Update update, FindAndModifyOptions options) { + Class entityClass, UpdateDefinition update, FindAndModifyOptions options) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); + increaseVersionForUpdateIfNecessary(entity, update); return Mono.defer(() -> { - increaseVersionForUpdateIfNecessary(entity, update); - Document mappedQuery = queryMapper.getMappedObject(query, entity); - Document mappedUpdate = updateMapper.getMappedObject(update.getUpdateObject(), entity); + + Object mappedUpdate = new Document(); + if (update instanceof AggregationUpdate) { + + AggregationOperationContext context = entityClass != null + ? new RelaxedTypeBasedAggregationOperationContext(entityClass, mappingContext, queryMapper) + : Aggregation.DEFAULT_CONTEXT; + + mappedUpdate = new AggregationUtil(queryMapper, mappingContext).createPipeline((Aggregation) update, context); + } else { + mappedUpdate = updateMapper.getMappedObject(update.getUpdateObject(), entity); + } if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format( @@ -2928,7 +2973,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private final Document query; private final Document fields; private final Document sort; - private final Document update; + private final Object update; private final List arrayFilters; private final FindAndModifyOptions options; @@ -2947,7 +2992,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati FindOneAndUpdateOptions findOneAndUpdateOptions = convertToFindOneAndUpdateOptions(options, fields, sort, arrayFilters); - return collection.findOneAndUpdate(query, update, findOneAndUpdateOptions); + if (update instanceof Document) { + return collection.findOneAndUpdate(query, (Document) update, findOneAndUpdateOptions); + } else if (update instanceof List) { + return collection.findOneAndUpdate(query, (List) update, findOneAndUpdateOptions); + } + return Flux.error(new IllegalArgumentException("doh - that does not work")); } private static FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOptions options, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java index be3fa10e2..7739f9980 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import reactor.core.publisher.Mono; import org.springframework.data.mongodb.core.query.Query; @@ -123,8 +124,22 @@ public interface ReactiveUpdateOperation { * @param update must not be {@literal null}. * @return new instance of {@link TerminatingUpdate}. Never {@literal null}. * @throws IllegalArgumentException if update is {@literal null}. + * @since 2.3 */ - TerminatingUpdate apply(org.springframework.data.mongodb.core.query.Update update); + TerminatingUpdate apply(org.springframework.data.mongodb.core.query.UpdateDefinition update); + + /** + * Set the {@link org.springframework.data.mongodb.core.query.Update} to be applied. + * + * @param update must not be {@literal null}. + * @return new instance of {@link TerminatingUpdate}. Never {@literal null}. + * @throws IllegalArgumentException if update is {@literal null}. + * @deprecated since 2.3 in favor of {@link #apply(UpdateDefinition)}. + */ + @Deprecated + default TerminatingUpdate apply(org.springframework.data.mongodb.core.query.Update update) { + return apply((UpdateDefinition) update); + } /** * Specify {@code replacement} object. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java index 6030606b1..22c1ad689 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java @@ -63,7 +63,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { @NonNull ReactiveMongoTemplate template; @NonNull Class domainType; Query query; - org.springframework.data.mongodb.core.query.Update update; + org.springframework.data.mongodb.core.query.UpdateDefinition update; @Nullable String collection; @Nullable FindAndModifyOptions findAndModifyOptions; @Nullable FindAndReplaceOptions findAndReplaceOptions; @@ -72,10 +72,10 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { /* * (non-Javadoc) - * @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithUpdate#apply(org.springframework.data.mongodb.core.query.Update) + * @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithUpdate#apply(org.springframework.data.mongodb.core.query.UpdateDefinition) */ @Override - public TerminatingUpdate apply(org.springframework.data.mongodb.core.query.Update update) { + public TerminatingUpdate apply(org.springframework.data.mongodb.core.query.UpdateDefinition update) { Assert.notNull(update, "Update must not be null!"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java index 32f2f6e9a..70d2987ca 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java @@ -115,6 +115,17 @@ public class Aggregation { return new Aggregation(operations); } + /** + * Creates a new {@link AggregationUpdate} from the given {@link AggregationOperation}s. + * + * @param operations can be {@literal empty} but must not be {@literal null}. + * @return new instance of {@link AggregationUpdate}. + * @since 2.3 + */ + public static AggregationUpdate newUpdate(AggregationOperation... operations) { + return AggregationUpdate.from(Arrays.asList(operations)); + } + /** * Returns a copy of this {@link Aggregation} with the given {@link AggregationOptions} set. Note that options are * supported in MongoDB version 2.6+. @@ -181,13 +192,12 @@ public class Aggregation { /** * Creates a new {@link Aggregation} from the given {@link AggregationOperation}s. * - * @param aggregationOperations must not be {@literal null} or empty. + * @param aggregationOperations must not be {@literal null}. * @param options must not be {@literal null} or empty. */ protected Aggregation(List aggregationOperations, AggregationOptions options) { Assert.notNull(aggregationOperations, "AggregationOperations must not be null!"); - Assert.isTrue(!aggregationOperations.isEmpty(), "At least one AggregationOperation has to be provided"); Assert.notNull(options, "AggregationOptions must not be null!"); // check $out is the last operation if it exists diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java new file mode 100644 index 000000000..7cc230466 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java @@ -0,0 +1,332 @@ +/* + * Copyright 2019 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.core.aggregation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.bson.Document; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.SerializationUtils; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Abstraction for an {@code db.collection.update()} using an aggregation pipeline for a more expressive update + * statement expressing conditional updates based on current field values or updating one field using the value of + * another field(s). + * + *
+ * AggregationUpdate update = AggregationUpdate.update().set("average")
+ * 		.toValue(ArithmeticOperators.valueOf("tests").avg()).set("grade")
+ * 		.toValue(ConditionalOperators
+ * 				.switchCases(CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(90)).then("A"),
+ * 						CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(80)).then("B"),
+ * 						CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(70)).then("C"),
+ * 						CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(60)).then("D"))
+ * 				.defaultTo("F"));
+ * 
+ * + * The above sample is equivalent to the JSON update statement + * + *
+ * db.collection.update(
+ *    { },
+ *    [
+ *      { $set: { average : { $avg: "$tests" } } },
+ *      { $set: { grade: { $switch: {
+ *                            branches: [
+ *                                { case: { $gte: [ "$average", 90 ] }, then: "A" },
+ *                                { case: { $gte: [ "$average", 80 ] }, then: "B" },
+ *                                { case: { $gte: [ "$average", 70 ] }, then: "C" },
+ *                                { case: { $gte: [ "$average", 60 ] }, then: "D" }
+ *                            ],
+ *                            default: "F"
+ *      } } } }
+ *    ],
+ *    { multi: true }
+ * )
+ * 
+ * + * @author Christoph Strobl + * @see
MongoDB + * Reference Documentation + * @since 2.3 + */ +public class AggregationUpdate extends Aggregation implements UpdateDefinition { + + private boolean isolated = false; + private Set keysTouched = new HashSet<>(); + + /** + * Create new {@link AggregationUpdate}. + */ + public AggregationUpdate() { + this(new ArrayList<>()); + } + + /** + * Create new {@link AggregationUpdate} with the given aggregation pipeline to apply. + * + * @param pipeline must not be {@literal null}. + */ + private AggregationUpdate(List pipeline) { + + super(pipeline); + + for (AggregationOperation operation : pipeline) { + if (operation instanceof FieldsExposingAggregationOperation) { + ((FieldsExposingAggregationOperation) operation).getFields().forEach(it -> { + if (it instanceof Field) { + keysTouched.add(((Field) it).getName()); + } else { + keysTouched.add(it.toString()); + } + }); + } + } + } + + /** + * Start defining the update pipeline to execute. + * + * @return new instance of {@link AggregationUpdate}. + */ + public static AggregationUpdate update() { + return new AggregationUpdate(); + } + + /** + * Create a new AggregationUpdate from the given {@link AggregationOperation}s. + * + * @return new instance of {@link AggregationUpdate}. + */ + public static AggregationUpdate from(List pipeline) { + return new AggregationUpdate(pipeline); + } + + /** + * Adds new fields to documents. {@code $set} outputs documents that contain all existing fields from the input + * documents and newly added fields. + * + * @param setOperation must not be {@literal null}. + * @return this. + * @see $set Aggregation Reference + */ + public AggregationUpdate set(SetOperation setOperation) { + + Assert.notNull(setOperation, "SetOperation must not be null!"); + + setOperation.getFields().forEach(it -> { + if (it instanceof Field) { + keysTouched.add(((Field) it).getName()); + } else { + keysTouched.add(it.toString()); + } + }); + operations.add(setOperation); + return this; + } + + /** + * {@code $unset} removes/excludes fields from documents. + * + * @param unsetOperation must not be {@literal null}. + * @return this. + * @see $unset Aggregation + * Reference + */ + public AggregationUpdate unset(UnsetOperation unsetOperation) { + + Assert.notNull(unsetOperation, "UnsetOperation must not be null!"); + + operations.add(unsetOperation); + keysTouched.addAll(unsetOperation.removedFieldNames()); + return this; + } + + /** + * {@code $replaceWith} replaces the input document with the specified document. The operation replaces all existing + * fields in the input document, including the _id field. + * + * @param replaceWithOperation + * @return this. + * @see $replaceWith Aggregation + * Reference + */ + public AggregationUpdate replaceWith(ReplaceWithOperation replaceWithOperation) { + + Assert.notNull(replaceWithOperation, "ReplaceWithOperation must not be null!"); + operations.add(replaceWithOperation); + return this; + } + + /** + * {@code $replaceWith} replaces the input document with the value. + * + * @param value must not be {@literal null}. + * @return this. + */ + public AggregationUpdate replaceWith(Object value) { + + Assert.notNull(value, "Value must not be null!"); + return replaceWith(ReplaceWithOperation.replaceWithValue(value)); + } + + /** + * Fluent API variant for {@code $set} adding a single {@link SetOperation pipeline operation} every time. To update + * multiple fields within one {@link SetOperation} use {@link #set(SetOperation)}. + * + * @param key must not be {@literal null}. + * @return new instance of {@link SetValueAppender}. + * @see #set(SetOperation) + */ + public SetValueAppender set(String key) { + + Assert.notNull(key, "Key must not be null!"); + + return new SetValueAppender() { + + @Override + public AggregationUpdate toValue(@Nullable Object value) { + return set(SetOperation.builder().set(key).toValue(value)); + } + + @Override + public AggregationUpdate toValueOf(Object value) { + + Assert.notNull(value, "Value must not be null!"); + return set(SetOperation.builder().set(key).toValueOf(value)); + } + }; + } + + /** + * Short for {@link #unset(UnsetOperation)}. + * + * @param keys + * @return + */ + public AggregationUpdate unset(String... keys) { + + Assert.notNull(keys, "Keys must not be null!"); + Assert.noNullElements(keys, "Keys must not contain null elements."); + + return unset(new UnsetOperation(Arrays.stream(keys).map(Fields::field).collect(Collectors.toList()))); + } + + /** + * Prevents a write operation that affects multiple documents from yielding to other reads or writes + * once the first document is written.
+ * Use with {@link org.springframework.data.mongodb.core.MongoOperations#updateMulti(Query, Update, Class)}. + * + * @return never {@literal null}. + */ + public AggregationUpdate isolated() { + + isolated = true; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.query.UpdateDefinition#isIsolated() + */ + @Override + public Boolean isIsolated() { + return isolated; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.query.UpdateDefinition#getUpdateObject() + */ + @Override + public Document getUpdateObject() { + return new Document("", toPipeline(Aggregation.DEFAULT_CONTEXT)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.query.UpdateDefinition#modifies(java.lang.String) + */ + @Override + public boolean modifies(String key) { + return keysTouched.contains(key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.query.UpdateDefinition#inc(java.lang.String) + */ + @Override + public void inc(String key) { + set(new SetOperation(key, ArithmeticOperators.valueOf(key).add(1))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.query.UpdateDefinition#getArrayFilters() + */ + @Override + public List getArrayFilters() { + return Collections.emptyList(); + } + + @Override + public String toString() { + + String target = "[\n"; + target += StringUtils.collectionToDelimitedString(toPipeline(Aggregation.DEFAULT_CONTEXT).stream() + .map(SerializationUtils::serializeToJsonSafely).collect(Collectors.toList()), ",\n"); + target += "\n]"; + return target; + } + + /** + * Fluent API AggregationUpdate builder. + * + * @author Christoph Strobl + * @since 2.3 + */ + public interface SetValueAppender { + + /** + * Define the target value as is. + * + * @param value can be {@literal null}. + * @return never {@literal null}. + */ + AggregationUpdate toValue(@Nullable Object value); + + /** + * Define the target value as value, an {@link AggregationExpression} or a {@link Field} reference. + * + * @param value can be {@literal null}. + * @return never {@literal null}. + */ + AggregationUpdate toValueOf(Object value); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RelaxedTypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RelaxedTypeBasedAggregationOperationContext.java new file mode 100644 index 000000000..dd96750f7 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RelaxedTypeBasedAggregationOperationContext.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 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.core.aggregation; + +import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; + +/** + * A {@link TypeBasedAggregationOperationContext} with less restrictive field reference handling, suppressing + * {@link InvalidPersistentPropertyPath} exceptions when resolving mapped field names. + * + * @author Christoph Strobl + * @since 2.3 + */ +public class RelaxedTypeBasedAggregationOperationContext extends TypeBasedAggregationOperationContext { + + /** + * Creates a new {@link TypeBasedAggregationOperationContext} for the given type, {@link MappingContext} and + * {@link QueryMapper}. + * + * @param type must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @param mapper must not be {@literal null}. + */ + public RelaxedTypeBasedAggregationOperationContext(Class type, + MappingContext, MongoPersistentProperty> mappingContext, QueryMapper mapper) { + super(type, mappingContext, mapper); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext#getReferenceFor(rg.springframework.data.mongodb.core.aggregation.Field) + */ + @Override + protected FieldReference getReferenceFor(Field field) { + + try { + return super.getReferenceFor(field); + } catch (InvalidPersistentPropertyPath e) { + return new DirectFieldReference(new ExposedField(field, true)); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java index cab2e4ff0..00c7bbc22 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java @@ -33,7 +33,8 @@ import org.springframework.util.Assert; * @author Mark Paluch * @author Christoph Strobl * @since 1.10 - * @see MongoDB Aggregation Framework: $replaceRoot + * @see MongoDB Aggregation + * Framework: $replaceRoot */ public class ReplaceRootOperation implements FieldsExposingAggregationOperation { @@ -82,7 +83,7 @@ public class ReplaceRootOperation implements FieldsExposingAggregationOperation */ @Override public Document toDocument(AggregationOperationContext context) { - return new Document("$replaceRoot", new Document("newRoot", replacement.toDocumentExpression(context))); + return new Document("$replaceRoot", new Document("newRoot", getReplacement().toDocumentExpression(context))); } /* (non-Javadoc) @@ -93,6 +94,16 @@ public class ReplaceRootOperation implements FieldsExposingAggregationOperation return ExposedFields.from(); } + /** + * Obtain the {@link Replacement}. + * + * @return never {@literal null}. + * @since 2.3 + */ + protected Replacement getReplacement() { + return replacement; + } + /** * Builder for {@link ReplaceRootOperation}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceWithOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceWithOperation.java new file mode 100644 index 000000000..81454326b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceWithOperation.java @@ -0,0 +1,95 @@ +/* + * Copyright 2019 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.core.aggregation; + +import java.util.Collection; +import java.util.stream.Collectors; + +import org.bson.Document; +import org.springframework.util.Assert; + +/** + * Encapsulates the aggregation framework {@code $replaceRoot}-operation.
+ * The operation replaces all existing fields including the {@code id} field with @{code $replaceWith}. This way it is + * possible to promote an embedded document to the top-level or specify a new document. + * + * @author Christoph Strobl + * @since 2.3 + * @see MongoDB Aggregation + * Framework: $replaceWith + */ +public class ReplaceWithOperation extends ReplaceRootOperation { + + /** + * Creates new instance of {@link ReplaceWithOperation}. + * + * @param replacement must not be {@literal null}. + */ + public ReplaceWithOperation(Replacement replacement) { + super(replacement); + } + + /** + * Creates new instance of {@link ReplaceWithOperation}. + * + * @param value must not be {@literal null}. + * @return new instance of {@link ReplaceWithOperation}. + */ + public static ReplaceWithOperation replaceWithValue(Object value) { + return new ReplaceWithOperation((ctx) -> value); + } + + /** + * Creates new instance of {@link ReplaceWithOperation} treating a given {@link String} {@literal value} as a + * {@link Field field reference}. + * + * @param value must not be {@literal null}. + * @return + */ + public static ReplaceWithOperation replaceWithValueOf(Object value) { + + Assert.notNull(value, "Value must not be null!"); + return new ReplaceWithOperation((ctx) -> { + + Object target = value instanceof String ? Fields.field((String) value) : value; + return computeValue(target, ctx); + }); + } + + private static Object computeValue(Object value, AggregationOperationContext context) { + + if (value instanceof Field) { + return context.getReference((Field) value).toString(); + } + if (value instanceof AggregationExpression) { + return ((AggregationExpression) value).toDocument(context); + } + if (value instanceof Collection) { + return ((Collection) value).stream().map(it -> computeValue(it, context)).collect(Collectors.toList()); + } + + return value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { + return context.getMappedObject(new Document("$replaceWith", getReplacement().toDocumentExpression(context))); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java new file mode 100644 index 000000000..0c7933eec --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java @@ -0,0 +1,250 @@ +/* + * Copyright 2019 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.core.aggregation; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.bson.Document; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; +import org.springframework.data.mongodb.core.aggregation.SetOperation.FieldAppender.ValueAppender; +import org.springframework.lang.Nullable; + +/** + * Adds new fields to documents. {@code $set} outputs documents that contain all existing fields from the input + * documents and newly added fields. + * + *
+ * SetOperation.set("totalHomework").toValue("A+").and().set("totalQuiz").toValue("B-")
+ * 
+ * + * @author Christoph Strobl + * @since 2.3 + * @see MongoDB Aggregation Framework: + * $set + */ +public class SetOperation implements InheritsFieldsAggregationOperation { + + private Map valueMap; + private ExposedFields exposedFields = ExposedFields.empty(); + + /** + * Create new instance of {@link SetOperation} adding map keys as exposed fields. + * + * @param source must not be {@literal null}. + */ + private SetOperation(Map source) { + + this.valueMap = new LinkedHashMap<>(source); + for (Object key : source.keySet()) { + this.exposedFields = add(key); + } + } + + /** + * Create new instance of {@link SetOperation} + * + * @param field must not be {@literal null}. + * @param value can be {@literal null}. + */ + public SetOperation(Object field, @Nullable Object value) { + this(Collections.singletonMap(field, value)); + } + + /** + * Define the {@link SetOperation} via {@link FieldAppender}. + * + * @return new instance of {@link FieldAppender}. + */ + public static FieldAppender builder() { + return new FieldAppender(); + } + + /** + * Concatenate another field to set. + * + * @param field must not be {@literal null}. + * @return new instance of {@link ValueAppender}. + */ + public static ValueAppender set(String field) { + return new FieldAppender().set(field); + } + + /** + * Append the value for a specific field to the operation. + * + * @param field the target field to set. + * @param value the value to assign. + * @return new instance of {@link SetOperation}. + */ + public SetOperation set(Object field, Object value) { + + LinkedHashMap target = new LinkedHashMap<>(this.valueMap); + target.put(field, value); + + return new SetOperation(target); + } + + /** + * Concatenate additional fields to set. + * + * @return new instance of {@link FieldAppender}. + */ + public FieldAppender and() { + return new FieldAppender(this.valueMap); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { + + InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext( + exposedFields, context); + + if (valueMap.size() == 1) { + return context + .getMappedObject(new Document("$set", toSetEntry(valueMap.entrySet().iterator().next(), operationContext))); + } + + Document $set = new Document(); + valueMap.entrySet().stream().map(it -> toSetEntry(it, operationContext)).forEach($set::putAll); + return context.getMappedObject(new Document("$set", $set)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields() + */ + @Override + public ExposedFields getFields() { + return exposedFields; + } + + private ExposedFields add(Object field) { + + if (field instanceof Field) { + return exposedFields.and(new ExposedField((Field) field, true)); + } + if (field instanceof String) { + return exposedFields.and(new ExposedField(Fields.field((String) field), true)); + } + + throw new IllegalArgumentException(String.format("Expected %s to be a field/property.", field)); + } + + private static Document toSetEntry(Entry entry, AggregationOperationContext context) { + + String field = entry.getKey() instanceof String ? context.getReference((String) entry.getKey()).getRaw() + : context.getReference((Field) entry.getKey()).getRaw(); + + Object value = computeValue(entry.getValue(), context); + + return new Document(field, value); + } + + private static Object computeValue(Object value, AggregationOperationContext context) { + + if (value instanceof Field) { + return context.getReference((Field) value).toString(); + } + if (value instanceof AggregationExpression) { + return ((AggregationExpression) value).toDocument(context); + } + if (value instanceof Collection) { + return ((Collection) value).stream().map(it -> computeValue(it, context)).collect(Collectors.toList()); + } + + return value; + } + + /** + * @author Christoph Strobl + * @since 2.3 + */ + public static class FieldAppender { + + private final Map valueMap; + + private FieldAppender() { + this.valueMap = new LinkedHashMap<>(); + } + + private FieldAppender(Map source) { + this.valueMap = new LinkedHashMap<>(source); + } + + /** + * Define the field to set. + * + * @param field must not be {@literal null}. + * @return new instance of {@link ValueAppender}. + */ + public ValueAppender set(String field) { + + return new ValueAppender() { + + @Override + public SetOperation toValue(Object value) { + + valueMap.put(field, value); + return FieldAppender.this.build(); + } + + @Override + public SetOperation toValueOf(Object value) { + + valueMap.put(field, value instanceof String ? Fields.fields((String) value) : value); + return FieldAppender.this.build(); + } + }; + } + + private SetOperation build() { + return new SetOperation(valueMap); + } + + /** + * @author Christoph Strobl + * @since 2.3 + */ + public interface ValueAppender { + + /** + * Define the value to assign as is. + * + * @param value can be {@literal null}. + * @return new instance of {@link SetOperation}. + */ + SetOperation toValue(@Nullable Object value); + + /** + * Define the value to assign. Plain {@link String} values are treated as {@link Field field references}. + * + * @param value must not be {@literal null}. + * @return new instance of {@link SetOperation}. + */ + SetOperation toValueOf(Object value); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java index 5595c4e47..75a7ff41d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.List; import org.bson.Document; - import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; @@ -128,7 +127,7 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio return Fields.fields(fields.toArray(new String[0])); } - private FieldReference getReferenceFor(Field field) { + protected FieldReference getReferenceFor(Field field) { PersistentPropertyPath propertyPath = mappingContext .getPersistentPropertyPath(field.getTarget(), type); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java new file mode 100644 index 000000000..f2db18ad4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java @@ -0,0 +1,144 @@ +/* + * Copyright 2019 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.core.aggregation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.bson.Document; +import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Removes fields from documents. + * + * @author Christoph Strobl + * @since 2.3 + * @see MongoDB Aggregation Framework: + * $unset + */ +public class UnsetOperation implements InheritsFieldsAggregationOperation { + + private final Collection fields; + + /** + * Create new instance of {@link UnsetOperation}. + * + * @param fields must not be {@literal null}. + */ + public UnsetOperation(Collection fields) { + + Assert.notNull(fields, "Fields must not be null!"); + Assert.noNullElements(fields, "Fields must not contain null values."); + + this.fields = fields; + } + + /** + * Create new instance of {@link UnsetOperation}. + * + * @param fields must not be {@literal null}. + * @return new instance of {@link UnsetOperation}. + */ + public static UnsetOperation unset(String... fields) { + return new UnsetOperation(Arrays.asList(fields)); + } + + /** + * Also unset the given fields. + * + * @param fields must not be {@literal null}. + * @return new instance of {@link UnsetOperation}. + */ + public UnsetOperation and(String... fields) { + + List target = new ArrayList<>(this.fields); + CollectionUtils.mergeArrayIntoCollection(fields, target); + return new UnsetOperation(target); + } + + /** + * Also unset the given fields. + * + * @param fields must not be {@literal null}. + * @return new instance of {@link UnsetOperation}. + */ + public UnsetOperation and(Field... fields) { + + List target = new ArrayList<>(this.fields); + CollectionUtils.mergeArrayIntoCollection(fields, target); + return new UnsetOperation(target); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields() + */ + @Override + public ExposedFields getFields() { + return ExposedFields.from(); + } + + Collection removedFieldNames() { + + List fieldNames = new ArrayList<>(fields.size()); + for (Object it : fields) { + if (it instanceof Field) { + fieldNames.add(((Field) it).getName()); + } else { + fieldNames.add(it.toString()); + } + } + return fieldNames; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { + + if (fields.size() == 1) { + return new Document("$unset", computeFieldName(fields.iterator().next(), context)); + } + + return new Document("$unset", + fields.stream().map(it -> computeFieldName(it, context)).collect(Collectors.toList())); + } + + private Object computeFieldName(Object field, AggregationOperationContext context) { + + if (field instanceof Field) { + return context.getReference((Field) field).getRaw(); + } + + if (field instanceof AggregationExpression) { + return ((AggregationExpression) field).toDocument(context); + } + + if (field instanceof String) { + return context.getReference((String) field).getRaw(); + } + + return field; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 79e239fbb..0e1d97a49 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -64,6 +64,13 @@ import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; +import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators; +import org.springframework.data.mongodb.core.aggregation.ComparisonOperators.Gte; +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators; +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Switch.CaseOperator; +import org.springframework.data.mongodb.core.aggregation.Fields; +import org.springframework.data.mongodb.core.aggregation.SetOperation; import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; @@ -1713,6 +1720,101 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); } + @Test // DATAMONGO-2331 + public void updateShouldAllowAggregationExpressions() { + + AggregationUpdate update = new AggregationUpdate().set("total") + .toValue(ArithmeticOperators.valueOf("val1").sum().and("val2")); + + template.updateFirst(new BasicQuery("{}"), update, Wrapper.class); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).isEqualTo( + Collections.singletonList(Document.parse("{ $set : { total : { $sum : [ \"$val1\",\"$val2\" ] } } }"))); + } + + @Test // DATAMONGO-2331 + public void updateShouldAllowMultipleAggregationExpressions() { + + AggregationUpdate update = new AggregationUpdate() // + .set("average").toValue(ArithmeticOperators.valueOf("tests").avg()) // + .set("grade").toValue(ConditionalOperators.switchCases( // + CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(90)).then("A"), // + CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(80)).then("B"), // + CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(70)).then("C"), // + CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(60)).then("D") // + ) // + .defaultTo("F"));// + + template.updateFirst(new BasicQuery("{}"), update, Wrapper.class); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).containsExactly(Document.parse("{ $set: { average : { $avg: \"$tests\" } } }"), + Document.parse("{ $set: { grade: { $switch: {\n" + " branches: [\n" + + " { case: { $gte: [ \"$average\", 90 ] }, then: \"A\" },\n" + + " { case: { $gte: [ \"$average\", 80 ] }, then: \"B\" },\n" + + " { case: { $gte: [ \"$average\", 70 ] }, then: \"C\" },\n" + + " { case: { $gte: [ \"$average\", 60 ] }, then: \"D\" }\n" + + " ],\n" + " default: \"F\"\n" + " } } } }")); + } + + @Test // DATAMONGO-2331 + public void updateShouldMapAggregationExpressionToDomainType() { + + AggregationUpdate update = new AggregationUpdate().set("name") + .toValue(ArithmeticOperators.valueOf("val1").sum().and("val2")); + + template.updateFirst(new BasicQuery("{}"), update, Jedi.class); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).isEqualTo( + Collections.singletonList(Document.parse("{ $set : { firstname : { $sum:[ \"$val1\",\"$val2\" ] } } }"))); + } + + @Test // DATAMONGO-2331 + public void updateShouldPassOnUnsetCorrectly() { + + SetOperation setOperation = SetOperation.builder().set("status").toValue("Modified").and().set("comments") + .toValue(Fields.fields("misc1").and("misc2").asList()); + AggregationUpdate update = new AggregationUpdate(); + update.set(setOperation); + update.unset("misc1", "misc2"); + + template.updateFirst(new BasicQuery("{}"), update, Wrapper.class); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).isEqualTo( + Arrays.asList(Document.parse("{ $set: { status: \"Modified\", comments: [ \"$misc1\", \"$misc2\" ] } }"), + Document.parse("{ $unset: [ \"misc1\", \"misc2\" ] }"))); + } + + @Test // DATAMONGO-2331 + public void updateShouldMapAggregationUnsetToDomainType() { + + AggregationUpdate update = new AggregationUpdate(); + update.unset("name"); + + template.updateFirst(new BasicQuery("{}"), update, Jedi.class); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).isEqualTo(Collections.singletonList(Document.parse("{ $unset : \"firstname\" }"))); + } + class AutogenerateableId { @Id BigInteger id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUpdateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUpdateTests.java new file mode 100644 index 000000000..5b8367035 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUpdateTests.java @@ -0,0 +1,338 @@ +/* + * Copyright 2019 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.core; + +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; +import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators; +import org.springframework.data.mongodb.core.aggregation.ReplaceWithOperation; +import org.springframework.data.mongodb.core.aggregation.SetOperation; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.test.util.MongoTestUtils; + +import com.mongodb.MongoClient; +import com.mongodb.client.MongoCollection; + +/** + * @author Christoph Strobl + */ +public class MongoTemplateUpdateTests { + + static final String DB_NAME = "update-test"; + + MongoClient client; + MongoTemplate template; + + @Before + public void setUp() { + + client = MongoTestUtils.replSetClient(); + template = new MongoTemplate(new SimpleMongoDbFactory(client, DB_NAME)); + + MongoTestUtils.createOrReplaceCollection(DB_NAME, template.getCollectionName(Score.class), client); + MongoTestUtils.createOrReplaceCollection(DB_NAME, template.getCollectionName(Versioned.class), client); + MongoTestUtils.createOrReplaceCollection(DB_NAME, template.getCollectionName(Book.class), client); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateWithSet() { + + Score score1 = new Score(1, "Maya", Arrays.asList(10, 5, 10), Arrays.asList(10, 8), 0); + Score score2 = new Score(2, "Ryan", Arrays.asList(5, 6, 5), Arrays.asList(8, 8), 8); + + template.insertAll(Arrays.asList(score1, score2)); + + AggregationUpdate update = new AggregationUpdate().set(SetOperation.builder() // + .set("totalHomework").toValueOf(ArithmeticOperators.valueOf("homework").sum()).and() // + .set("totalQuiz").toValueOf(ArithmeticOperators.valueOf("quiz").sum())) // + .set(SetOperation.builder() // + .set("totalScore") + .toValueOf(ArithmeticOperators.valueOf("totalHomework").add("totalQuiz").add("extraCredit"))); + + template.update(Score.class).apply(update).all(); + + assertThat(collection(Score.class).find(new org.bson.Document()).into(new ArrayList<>())).containsExactlyInAnyOrder( // + org.bson.Document.parse( + "{\"_id\" : 1, \"student\" : \"Maya\", \"homework\" : [ 10, 5, 10 ], \"quiz\" : [ 10, 8 ], \"extraCredit\" : 0, \"totalHomework\" : 25, \"totalQuiz\" : 18, \"totalScore\" : 43, \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Score\"}"), + org.bson.Document.parse( + "{ \"_id\" : 2, \"student\" : \"Ryan\", \"homework\" : [ 5, 6, 5 ], \"quiz\" : [ 8, 8 ], \"extraCredit\" : 8, \"totalHomework\" : 16, \"totalQuiz\" : 16, \"totalScore\" : 40, \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Score\"}")); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateWithSetToValue() { + + Book one = new Book(); + one.id = 1; + one.author = new Author("John", "Backus"); + + template.insertAll(Arrays.asList(one)); + + AggregationUpdate update = new AggregationUpdate().set("author").toValue(new Author("Ada", "Lovelace")); + + template.update(Book.class).matching(Query.query(Criteria.where("id").is(one.id))).apply(update).all(); + + assertThat(all(Book.class)).containsExactlyInAnyOrder(org.bson.Document.parse( + "{\"_id\" : 1, \"author\" : {\"first\" : \"Ada\", \"last\" : \"Lovelace\"}, \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Book\"}")); + } + + @Test // DATAMONGO-2331 + public void versionedAggregateUpdateWithSet() { + + Versioned source = template.insert(Versioned.class).one(new Versioned("id-1", "value-0")); + + AggregationUpdate update = new AggregationUpdate().set("value").toValue("changed"); + template.update(Versioned.class).matching(Query.query(Criteria.where("id").is(source.id))).apply(update).first(); + + assertThat( + collection(Versioned.class).find(new org.bson.Document("_id", source.id)).limit(1).into(new ArrayList<>())) + .containsExactly(new org.bson.Document("_id", source.id).append("version", 1L).append("value", "changed") + .append("_class", "org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Versioned")); + } + + @Test // DATAMONGO-2331 + public void versionedAggregateUpdateTouchingVersionProperty() { + + Versioned source = template.insert(Versioned.class).one(new Versioned("id-1", "value-0")); + + AggregationUpdate update = new AggregationUpdate() + .set(SetOperation.builder().set("value").toValue("changed").and().set("version").toValue(10L)); + template.update(Versioned.class).matching(Query.query(Criteria.where("id").is(source.id))).apply(update).first(); + + assertThat( + collection(Versioned.class).find(new org.bson.Document("_id", source.id)).limit(1).into(new ArrayList<>())) + .containsExactly(new org.bson.Document("_id", source.id).append("version", 10L).append("value", "changed") + .append("_class", "org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Versioned")); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateWithUnset() { + + Book antelopeAntics = new Book(); + antelopeAntics.id = 1; + antelopeAntics.title = "Antelope Antics"; + antelopeAntics.isbn = "0001122223334"; + antelopeAntics.author = new Author("Auntie", "An"); + antelopeAntics.stock = new ArrayList<>(); + antelopeAntics.stock.add(new Warehouse("A", 5)); + antelopeAntics.stock.add(new Warehouse("B", 15)); + + Book beesBabble = new Book(); + beesBabble.id = 2; + beesBabble.title = "Bees Babble"; + beesBabble.isbn = "999999999333"; + beesBabble.author = new Author("Bee", "Bumble"); + beesBabble.stock = new ArrayList<>(); + beesBabble.stock.add(new Warehouse("A", 2)); + beesBabble.stock.add(new Warehouse("B", 5)); + + template.insertAll(Arrays.asList(antelopeAntics, beesBabble)); + + AggregationUpdate update = new AggregationUpdate().unset("isbn", "stock"); + template.update(Book.class).apply(update).all(); + + assertThat(all(Book.class)).containsExactlyInAnyOrder( // + org.bson.Document.parse( + "{ \"_id\" : 1, \"title\" : \"Antelope Antics\", \"author\" : { \"last\" : \"An\", \"first\" : \"Auntie\" }, \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Book\" }"), + org.bson.Document.parse( + "{ \"_id\" : 2, \"title\" : \"Bees Babble\", \"author\" : { \"last\" : \"Bumble\", \"first\" : \"Bee\" }, \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Book\" }")); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateWithReplaceWith() { + + Book one = new Book(); + one.id = 1; + one.author = new Author("John", "Backus"); + + Book two = new Book(); + two.id = 2; + two.author = new Author("Grace", "Hopper"); + + template.insertAll(Arrays.asList(one, two)); + + AggregationUpdate update = new AggregationUpdate().replaceWith(ReplaceWithOperation.replaceWithValueOf("author")); + + template.update(Book.class).apply(update).all(); + + assertThat(all(Book.class)).containsExactlyInAnyOrder( + org.bson.Document.parse("{\"_id\" : 1, \"first\" : \"John\", \"last\" : \"Backus\"}"), + org.bson.Document.parse("{\"_id\" : 2, \"first\" : \"Grace\", \"last\" : \"Hopper\"}")); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateWithReplaceWithNewObject() { + + Book one = new Book(); + one.id = 1; + one.author = new Author("John", "Backus"); + + Book two = new Book(); + two.id = 2; + two.author = new Author("Grace", "Hopper"); + + template.insertAll(Arrays.asList(one, two)); + + AggregationUpdate update = new AggregationUpdate().replaceWith(new Author("Ada", "Lovelace")); + + template.update(Book.class).matching(Query.query(Criteria.where("id").is(one.id))).apply(update).all(); + + assertThat(all(Book.class)).containsExactlyInAnyOrder(org.bson.Document.parse( + "{\"_id\" : 1, \"first\" : \"Ada\", \"last\" : \"Lovelace\", \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Author\"}"), + org.bson.Document.parse( + "{\"_id\" : 2, \"author\" : {\"first\" : \"Grace\", \"last\" : \"Hopper\"}, \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Book\"}")); + } + + @Test // DATAMONGO-2331 + public void aggregationUpdateUpsertsCorrectly() { + + AggregationUpdate update = AggregationUpdate.update().set("title").toValue("The Burning White"); + + template.update(Book.class).matching(Query.query(Criteria.where("id").is(1))).apply(update).upsert(); + + assertThat(all(Book.class)) + .containsExactly(org.bson.Document.parse("{\"_id\" : 1, \"title\" : \"The Burning White\" }")); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateFirstMatch() { + + Book one = new Book(); + one.id = 1; + one.title = "The Blood Mirror"; + + Book two = new Book(); + two.id = 2; + two.title = "The Broken Eye"; + + template.insertAll(Arrays.asList(one, two)); + + template.update(Book.class).apply(AggregationUpdate.update().set("title").toValue("The Blinding Knife")).first(); + + assertThat(all(Book.class)).containsExactly(org.bson.Document.parse( + "{\"_id\" : 1, \"title\" : \"The Blinding Knife\", \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Book\"}"), + org.bson.Document.parse( + "{\"_id\" : 2, \"title\" : \"The Broken Eye\", \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Book\"}")); + } + + @Test // DATAMONGO-2331 + @Ignore("https://jira.mongodb.org/browse/JAVA-3432") + public void findAndModifyAppliesAggregationUpdateCorrectly() { + + Book one = new Book(); + one.id = 1; + one.title = "The Blood Mirror"; + + Book two = new Book(); + two.id = 2; + two.title = "The Broken Eye"; + + template.insertAll(Arrays.asList(one, two)); + + Book retrieved = template.update(Book.class).matching(Query.query(Criteria.where("id").is(one.id))) + .apply(AggregationUpdate.update().set("title").toValue("The Blinding Knife")).findAndModifyValue(); + assertThat(retrieved).isEqualTo(one); + + assertThat(all(Book.class)).containsExactly(org.bson.Document.parse( + "{\"_id\" : 1, \"title\" : \"The Blinding Knife\", \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Book\"}"), + org.bson.Document.parse( + "{\"_id\" : 2, \"title\" : \"The Broken Eye\", \"_class\" : \"org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Book\"}")); + + } + + private List all(Class type) { + return collection(type).find(new org.bson.Document()).into(new ArrayList<>()); + } + + private MongoCollection collection(Class type) { + return client.getDatabase(DB_NAME).getCollection(template.getCollectionName(type)); + } + + @Document("scores") + static class Score { + + Integer id; + String student; + List homework; + List quiz; + Integer extraCredit; + + public Score(Integer id, String student, List homework, List quiz, Integer extraCredit) { + + this.id = id; + this.student = student; + this.homework = homework; + this.quiz = quiz; + this.extraCredit = extraCredit; + } + } + + static class Versioned { + + String id; + @Version Long version; + String value; + + public Versioned(String id, String value) { + this.id = id; + this.value = value; + } + } + + static class Book { + + @Id Integer id; + String title; + String isbn; + Author author; + @Field("copies") Collection stock; + } + + static class Author { + + @Field("first") String firstname; + @Field("last") String lastname; + + public Author(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + } + + static class Warehouse { + + public Warehouse(String location, Integer qty) { + this.location = location; + this.qty = qty; + } + + @Field("warehouse") String location; + Integer qty; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 2d2d255a0..1b6f2f9e8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -26,6 +26,7 @@ import reactor.test.StepVerifier; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -51,6 +52,13 @@ import org.springframework.data.annotation.Id; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.data.mongodb.core.MongoTemplateUnitTests.AutogenerateableId; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; +import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators; +import org.springframework.data.mongodb.core.aggregation.ComparisonOperators.Gte; +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators; +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Switch.CaseOperator; +import org.springframework.data.mongodb.core.aggregation.Fields; +import org.springframework.data.mongodb.core.aggregation.SetOperation; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.mapping.Field; @@ -134,6 +142,8 @@ public class ReactiveMongoTemplateUnitTests { when(collection.countDocuments(any(), any(CountOptions.class))).thenReturn(Mono.just(0L)); when(collection.updateOne(any(), any(Bson.class), any(UpdateOptions.class))).thenReturn(updateResultPublisher); when(collection.updateMany(any(Bson.class), any(Bson.class), any())).thenReturn(updateResultPublisher); + when(collection.updateOne(any(), anyList(), any())).thenReturn(updateResultPublisher); + when(collection.updateMany(any(), anyList(), any())).thenReturn(updateResultPublisher); when(collection.findOneAndUpdate(any(), any(Bson.class), any(FindOneAndUpdateOptions.class))) .thenReturn(findAndUpdatePublisher); when(collection.findOneAndReplace(any(Bson.class), any(), any())).thenReturn(findPublisher); @@ -881,6 +891,101 @@ public class ReactiveMongoTemplateUnitTests { verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); } + @Test // DATAMONGO-2331 + public void updateShouldAllowAggregationExpressions() { + + AggregationUpdate update = new AggregationUpdate().set("total") + .toValue(ArithmeticOperators.valueOf("val1").sum().and("val2")); + + template.updateFirst(new BasicQuery("{}"), update, Wrapper.class).subscribe(); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).isEqualTo( + Collections.singletonList(Document.parse("{ $set : { total : { $sum : [ \"$val1\",\"$val2\" ] } } }"))); + } + + @Test // DATAMONGO-2331 + public void updateShouldAllowMultipleAggregationExpressions() { + + AggregationUpdate update = new AggregationUpdate() // + .set("average").toValue(ArithmeticOperators.valueOf("tests").avg()) // + .set("grade").toValue(ConditionalOperators.switchCases( // + CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(90)).then("A"), // + CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(80)).then("B"), // + CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(70)).then("C"), // + CaseOperator.when(Gte.valueOf("average").greaterThanEqualToValue(60)).then("D") // + ) // + .defaultTo("F"));// + + template.updateFirst(new BasicQuery("{}"), update, Wrapper.class).subscribe(); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).containsExactly(Document.parse("{ $set: { average : { $avg: \"$tests\" } } }"), + Document.parse("{ $set: { grade: { $switch: {\n" + " branches: [\n" + + " { case: { $gte: [ \"$average\", 90 ] }, then: \"A\" },\n" + + " { case: { $gte: [ \"$average\", 80 ] }, then: \"B\" },\n" + + " { case: { $gte: [ \"$average\", 70 ] }, then: \"C\" },\n" + + " { case: { $gte: [ \"$average\", 60 ] }, then: \"D\" }\n" + + " ],\n" + " default: \"F\"\n" + " } } } }")); + } + + @Test // DATAMONGO-2331 + public void updateShouldMapAggregationExpressionToDomainType() { + + AggregationUpdate update = new AggregationUpdate().set("name") + .toValue(ArithmeticOperators.valueOf("val1").sum().and("val2")); + + template.updateFirst(new BasicQuery("{}"), update, Jedi.class).subscribe(); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).isEqualTo( + Collections.singletonList(Document.parse("{ $set : { firstname : { $sum:[ \"$val1\",\"$val2\" ] } } }"))); + } + + @Test // DATAMONGO-2331 + public void updateShouldPassOnUnsetCorrectly() { + + SetOperation setOperation = SetOperation.builder().set("status").toValue("Modified").and().set("comments") + .toValue(Fields.fields("misc1").and("misc2").asList()); + AggregationUpdate update = new AggregationUpdate(); + update.set(setOperation); + update.unset("misc1", "misc2"); + + template.updateFirst(new BasicQuery("{}"), update, Wrapper.class).subscribe(); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).isEqualTo( + Arrays.asList(Document.parse("{ $set: { status: \"Modified\", comments: [ \"$misc1\", \"$misc2\" ] } }"), + Document.parse("{ $unset: [ \"misc1\", \"misc2\" ] }"))); + } + + @Test // DATAMONGO-2331 + public void updateShouldMapAggregationUnsetToDomainType() { + + AggregationUpdate update = new AggregationUpdate(); + update.unset("name"); + + template.updateFirst(new BasicQuery("{}"), update, Jedi.class).subscribe(); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + + verify(collection, times(1)).updateOne(any(org.bson.Document.class), captor.capture(), any(UpdateOptions.class)); + + assertThat(captor.getValue()).isEqualTo(Collections.singletonList(Document.parse("{ $unset : \"firstname\" }"))); + } + @Data @org.springframework.data.mongodb.core.mapping.Document(collection = "star-wars") static class Person { @@ -889,6 +994,11 @@ public class ReactiveMongoTemplateUnitTests { String firstname; } + class Wrapper { + + AutogenerateableId foo; + } + static class PersonExtended extends Person { String lastname; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUpdateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUpdateTests.java new file mode 100644 index 000000000..4d0a99e7c --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUpdateTests.java @@ -0,0 +1,337 @@ +/* + * Copyright 2019 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.core; + +import static org.assertj.core.api.Assertions.*; + +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; +import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators; +import org.springframework.data.mongodb.core.aggregation.ReplaceWithOperation; +import org.springframework.data.mongodb.core.aggregation.SetOperation; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.test.util.MongoTestUtils; + +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoCollection; + +/** + * @author Christoph Strobl + */ +public class ReactiveMongoTemplateUpdateTests { + + static final String DB_NAME = "reactive-update-test"; + + MongoClient client; + ReactiveMongoTemplate template; + + @Before + public void setUp() { + + client = MongoTestUtils.reactiveReplSetClient(); + template = new ReactiveMongoTemplate(new SimpleReactiveMongoDatabaseFactory(client, DB_NAME)); + + MongoTestUtils.createOrReplaceCollection(DB_NAME, template.getCollectionName(Score.class), client).then() + .as(StepVerifier::create).verifyComplete(); + MongoTestUtils.createOrReplaceCollection(DB_NAME, template.getCollectionName(Versioned.class), client).then() + .as(StepVerifier::create).verifyComplete(); + MongoTestUtils.createOrReplaceCollection(DB_NAME, template.getCollectionName(Book.class), client).then() + .as(StepVerifier::create).verifyComplete(); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateWithSet() { + + Score score1 = new Score(1, "Maya", Arrays.asList(10, 5, 10), Arrays.asList(10, 8), 0); + Score score2 = new Score(2, "Ryan", Arrays.asList(5, 6, 5), Arrays.asList(8, 8), 8); + + template.insertAll(Arrays.asList(score1, score2)).then().as(StepVerifier::create).verifyComplete(); + + AggregationUpdate update = new AggregationUpdate().set(SetOperation.builder() // + .set("totalHomework").toValueOf(ArithmeticOperators.valueOf("homework").sum()).and() // + .set("totalQuiz").toValueOf(ArithmeticOperators.valueOf("quiz").sum())) // + .set(SetOperation.builder() // + .set("totalScore") + .toValueOf(ArithmeticOperators.valueOf("totalHomework").add("totalQuiz").add("extraCredit"))); + + template.update(Score.class).apply(update).all().then().as(StepVerifier::create).verifyComplete(); + + Flux.from(collection(Score.class).find(new org.bson.Document())).collectList().as(StepVerifier::create) + .consumeNextWith(it -> { + + assertThat(it).containsExactlyInAnyOrder( // + org.bson.Document.parse( + "{\"_id\" : 1, \"student\" : \"Maya\", \"homework\" : [ 10, 5, 10 ], \"quiz\" : [ 10, 8 ], \"extraCredit\" : 0, \"totalHomework\" : 25, \"totalQuiz\" : 18, \"totalScore\" : 43, \"_class\" : \"org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Score\"}"), + org.bson.Document.parse( + "{ \"_id\" : 2, \"student\" : \"Ryan\", \"homework\" : [ 5, 6, 5 ], \"quiz\" : [ 8, 8 ], \"extraCredit\" : 8, \"totalHomework\" : 16, \"totalQuiz\" : 16, \"totalScore\" : 40, \"_class\" : \"org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Score\"}")); + }).verifyComplete(); + } + + @Test // DATAMONGO-2331 + public void versionedAggregateUpdateWithSet() { + + Versioned source = new Versioned("id-1", "value-0"); + template.insert(Versioned.class).one(source).then().as(StepVerifier::create).verifyComplete(); + + AggregationUpdate update = new AggregationUpdate().set("value").toValue("changed"); + template.update(Versioned.class).matching(Query.query(Criteria.where("id").is(source.id))).apply(update).first() + .then().as(StepVerifier::create).verifyComplete(); + + Flux.from(collection(Versioned.class).find(new org.bson.Document("_id", source.id)).limit(1)).collectList() + .as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it).containsExactly( + new org.bson.Document("_id", source.id).append("version", 1L).append("value", "changed").append("_class", + "org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Versioned")); + }).verifyComplete(); + } + + @Test // DATAMONGO-2331 + public void versionedAggregateUpdateTouchingVersionProperty() { + + Versioned source = new Versioned("id-1", "value-0"); + template.insert(Versioned.class).one(source).then().as(StepVerifier::create).verifyComplete(); + + AggregationUpdate update = new AggregationUpdate() + .set(SetOperation.builder().set("value").toValue("changed").and().set("version").toValue(10L)); + + template.update(Versioned.class).matching(Query.query(Criteria.where("id").is(source.id))).apply(update).first() + .then().as(StepVerifier::create).verifyComplete(); + + Flux.from(collection(Versioned.class).find(new org.bson.Document("_id", source.id)).limit(1)).collectList() + .as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it).containsExactly( + new org.bson.Document("_id", source.id).append("version", 10L).append("value", "changed").append("_class", + "org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Versioned")); + }).verifyComplete(); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateWithUnset() { + + Book antelopeAntics = new Book(); + antelopeAntics.id = 1; + antelopeAntics.title = "Antelope Antics"; + antelopeAntics.isbn = "0001122223334"; + antelopeAntics.author = new Author("Auntie", "An"); + antelopeAntics.stock = new ArrayList<>(); + antelopeAntics.stock.add(new Warehouse("A", 5)); + antelopeAntics.stock.add(new Warehouse("B", 15)); + + Book beesBabble = new Book(); + beesBabble.id = 2; + beesBabble.title = "Bees Babble"; + beesBabble.isbn = "999999999333"; + beesBabble.author = new Author("Bee", "Bumble"); + beesBabble.stock = new ArrayList<>(); + beesBabble.stock.add(new Warehouse("A", 2)); + beesBabble.stock.add(new Warehouse("B", 5)); + + template.insertAll(Arrays.asList(antelopeAntics, beesBabble)).then().as(StepVerifier::create).verifyComplete(); + + AggregationUpdate update = new AggregationUpdate().unset("isbn", "stock"); + template.update(Book.class).apply(update).all().then().as(StepVerifier::create).verifyComplete(); + + all(Book.class).collectList().as(StepVerifier::create).consumeNextWith(it -> { + + assertThat(it).containsExactlyInAnyOrder( // + org.bson.Document.parse( + "{ \"_id\" : 1, \"title\" : \"Antelope Antics\", \"author\" : { \"last\" : \"An\", \"first\" : \"Auntie\" }, \"_class\" : \"org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Book\" }"), + org.bson.Document.parse( + "{ \"_id\" : 2, \"title\" : \"Bees Babble\", \"author\" : { \"last\" : \"Bumble\", \"first\" : \"Bee\" }, \"_class\" : \"org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Book\" }")); + }).verifyComplete(); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateWithReplaceWith() { + + Book one = new Book(); + one.id = 1; + one.author = new Author("John", "Backus"); + + Book two = new Book(); + two.id = 2; + two.author = new Author("Grace", "Hopper"); + + template.insertAll(Arrays.asList(one, two)).then().as(StepVerifier::create).verifyComplete(); + ; + + AggregationUpdate update = new AggregationUpdate().replaceWith(ReplaceWithOperation.replaceWithValueOf("author")); + + template.update(Book.class).apply(update).all().then().as(StepVerifier::create).verifyComplete(); + + all(Book.class).collectList().as(StepVerifier::create).consumeNextWith(it -> { + + assertThat(it).containsExactlyInAnyOrder( + org.bson.Document.parse("{\"_id\" : 1, \"first\" : \"John\", \"last\" : \"Backus\"}"), + org.bson.Document.parse("{\"_id\" : 2, \"first\" : \"Grace\", \"last\" : \"Hopper\"}")); + }).verifyComplete(); + } + + @Test // DATAMONGO-2331 + public void aggregationUpdateUpsertsCorrectly() { + + AggregationUpdate update = AggregationUpdate.update().set("title").toValue("The Burning White"); + + template.update(Book.class).matching(Query.query(Criteria.where("id").is(1))).apply(update).upsert().then() + .as(StepVerifier::create).verifyComplete(); + + all(Book.class).collectList().as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it).containsExactly(org.bson.Document.parse("{\"_id\" : 1, \"title\" : \"The Burning White\" }")); + }).verifyComplete(); + } + + @Test // DATAMONGO-2331 + public void aggregateUpdateFirstMatch() { + + Book one = new Book(); + one.id = 1; + one.title = "The Blood Mirror"; + + Book two = new Book(); + two.id = 2; + two.title = "The Broken Eye"; + + template.insertAll(Arrays.asList(one, two)).then().as(StepVerifier::create).verifyComplete(); + + template.update(Book.class).apply(AggregationUpdate.update().set("title").toValue("The Blinding Knife")).first() + .then().as(StepVerifier::create).verifyComplete(); + + all(Book.class).collectList().as(StepVerifier::create).consumeNextWith(it -> { + + assertThat(it).containsExactly(org.bson.Document.parse( + "{\"_id\" : 1, \"title\" : \"The Blinding Knife\", \"_class\" : \"org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Book\"}"), + org.bson.Document.parse( + "{\"_id\" : 2, \"title\" : \"The Broken Eye\", \"_class\" : \"org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Book\"}")); + }).verifyComplete(); + } + + @Test // DATAMONGO-2331 + @Ignore("https://jira.mongodb.org/browse/JAVA-3432") + public void findAndModifyAppliesAggregationUpdateCorrectly() { + + Book one = new Book(); + one.id = 1; + one.title = "The Blood Mirror"; + + Book two = new Book(); + two.id = 2; + two.title = "The Broken Eye"; + + template.insertAll(Arrays.asList(one, two)).then().as(StepVerifier::create).verifyComplete(); + + template.update(Book.class) // + .matching(Query.query(Criteria.where("id").is(one.id))) // + .apply(AggregationUpdate.update().set("title").toValue("The Blinding Knife")) // + .findAndModify() // + .as(StepVerifier::create) // + .expectNext(one) // + .verifyComplete(); + + all(Book.class).collectList().as(StepVerifier::create).consumeNextWith(it -> { + + assertThat(it).containsExactly(org.bson.Document.parse( + "{\"_id\" : 1, \"title\" : \"The Blinding Knife\", \"_class\" : \"org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Book\"}"), + org.bson.Document.parse( + "{\"_id\" : 2, \"title\" : \"The Broken Eye\", \"_class\" : \"org.springframework.data.mongodb.core.ReactiveMongoTemplateUpdateTests$Book\"}")); + }).verifyComplete(); + + } + + private Flux all(Class type) { + return Flux.from(collection(type).find(new org.bson.Document())); + } + + private MongoCollection collection(Class type) { + return client.getDatabase(DB_NAME).getCollection(template.getCollectionName(type)); + } + + @Document("scores") + static class Score { + + Integer id; + String student; + List homework; + List quiz; + Integer extraCredit; + + public Score(Integer id, String student, List homework, List quiz, Integer extraCredit) { + + this.id = id; + this.student = student; + this.homework = homework; + this.quiz = quiz; + this.extraCredit = extraCredit; + } + } + + static class Versioned { + + String id; + @Version Long version; + String value; + + public Versioned(String id, String value) { + this.id = id; + this.value = value; + } + } + + static class Book { + + @Id Integer id; + String title; + String isbn; + Author author; + @Field("copies") Collection stock; + } + + static class Author { + + @Field("first") String firstname; + @Field("last") String lastname; + + public Author(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + } + + static class Warehouse { + + public Warehouse(String location, Integer qty) { + this.location = location; + this.qty = qty; + } + + @Field("warehouse") String location; + Integer qty; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdateUnitTests.java new file mode 100644 index 000000000..b76a1ba5e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdateUnitTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 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.core.aggregation; + +import static org.assertj.core.api.Assertions.*; + +import org.bson.Document; +import org.junit.Test; + +/** + * @author Christoph Strobl + */ +public class AggregationUpdateUnitTests { + + @Test // DATAMONGO-2331 + public void createPipelineWithMultipleStages() { + + assertThat(AggregationUpdate.update() // + .set("stage-1").toValue("value-1") // + .unset("stage-2") // + .set("stage-3").toValue("value-3") // + .toPipeline(Aggregation.DEFAULT_CONTEXT)) // + .containsExactly(new Document("$set", new Document("stage-1", "value-1")), + new Document("$unset", "stage-2"), new Document("$set", new Document("stage-3", "value-3"))); + } + + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReplaceWithOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReplaceWithOperationUnitTests.java new file mode 100644 index 000000000..5beab8b90 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReplaceWithOperationUnitTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 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.core.aggregation; + +import static org.assertj.core.api.Assertions.*; + +import org.bson.Document; +import org.junit.Test; + +/** + * Unit tests for {@link ReplaceRootOperation}. + * + * @author Christoph Strobl + */ +public class ReplaceWithOperationUnitTests { + + @Test // DATAMONGO-2331 + public void rejectsNullField() { + assertThatIllegalArgumentException().isThrownBy(() -> new ReplaceWithOperation(null)); + } + + @Test // DATAMONGO-2331 + public void shouldRenderValueCorrectly() { + + ReplaceWithOperation operation = ReplaceWithOperation.replaceWithValue(new Document("hello", "world")); + Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(dbObject).isEqualTo(Document.parse("{ $replaceWith : { hello: \"world\" } }")); + } + + @Test // DATAMONGO-2331 + public void shouldRenderExpressionCorrectly() { + + ReplaceWithOperation operation = ReplaceWithOperation.replaceWithValueOf(VariableOperators // + .mapItemsOf("array") // + .as("element") // + .andApply(AggregationFunctionExpressions.MULTIPLY.of("$$element", 10))); + + Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(dbObject).isEqualTo(Document.parse("{ $replaceWith : { " + + "$map : { input : \"$array\" , as : \"element\" , in : { $multiply : [ \"$$element\" , 10]} } " + "} }")); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java new file mode 100644 index 000000000..ef09839ae --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2019 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.core.aggregation; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.bson.Document; +import org.junit.Test; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +public class SetOperationUnitTests { + + @Test // DATAMONGO-2331 + public void raisesErrorOnNullField() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new SetOperation(null, "value")); + } + + @Test // DATAMONGO-2331 + public void rendersFieldReferenceCorrectly() { + + assertThat(new SetOperation("name", "value").toPipelineStages(contextFor(Scores.class))) + .containsExactly(Document.parse("{\"$set\" : {\"name\":\"value\"}}")); + } + + @Test // DATAMONGO-2331 + public void rendersMappedFieldReferenceCorrectly() { + + assertThat(new SetOperation("student", "value").toPipelineStages(contextFor(ScoresWithMappedField.class))) + .containsExactly(Document.parse("{\"$set\" : {\"student_name\":\"value\"}}")); + } + + @Test // DATAMONGO-2331 + public void rendersNestedMappedFieldReferenceCorrectly() { + + assertThat( + new SetOperation("scoresWithMappedField.student", "value").toPipelineStages(contextFor(ScoresWrapper.class))) + .containsExactly(Document.parse("{\"$set\" : {\"scoresWithMappedField.student_name\":\"value\"}}")); + } + + @Test // DATAMONGO-2331 + public void rendersTargetValueFieldReferenceCorrectly() { + + assertThat(new SetOperation("name", Fields.field("value")).toPipelineStages(contextFor(Scores.class))) + .containsExactly(Document.parse("{\"$set\" : {\"name\":\"$value\"}}")); + } + + @Test // DATAMONGO-2331 + public void rendersMappedTargetValueFieldReferenceCorrectly() { + + assertThat( + new SetOperation("student", Fields.field("homework")).toPipelineStages(contextFor(ScoresWithMappedField.class))) + .containsExactly(Document.parse("{\"$set\" : {\"student_name\":\"$home_work\"}}")); + } + + @Test // DATAMONGO-2331 + public void rendersNestedMappedTargetValueFieldReferenceCorrectly() { + + assertThat(new SetOperation("scoresWithMappedField.student", Fields.field("scoresWithMappedField.homework")) + .toPipelineStages(contextFor(ScoresWrapper.class))) + .containsExactly(Document + .parse("{\"$set\" : {\"scoresWithMappedField.student_name\":\"$scoresWithMappedField.home_work\"}}")); + } + + @Test // DATAMONGO-2331 + public void rendersTargetValueExpressionCorrectly() { + + assertThat(SetOperation.builder().set("totalHomework").toValueOf(ArithmeticOperators.valueOf("homework").sum()) + .toPipelineStages(contextFor(Scores.class))) + .containsExactly(Document.parse("{\"$set\" : {\"totalHomework\": { \"$sum\" : \"$homework\" }}}")); + } + + @Test // DATAMONGO-2331 + public void exposesFieldsCorrectly() { + + ExposedFields fields = SetOperation.builder().set("totalHomework").toValue("A+") // + .and() // + .set("totalQuiz").toValue("B-") // + .getFields(); + + assertThat(fields.getField("totalHomework")).isNotNull(); + assertThat(fields.getField("totalQuiz")).isNotNull(); + assertThat(fields.getField("does-not-exist")).isNull(); + } + + private static AggregationOperationContext contextFor(@Nullable Class type) { + + if (type == null) { + return Aggregation.DEFAULT_CONTEXT; + } + + MappingMongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, + new MongoMappingContext()); + mongoConverter.afterPropertiesSet(); + + return new RelaxedTypeBasedAggregationOperationContext(type, mongoConverter.getMappingContext(), + new QueryMapper(mongoConverter)); + } + + static class Scores { + + String student; + List homework; + } + + static class ScoresWithMappedField { + + @Field("student_name") String student; + @Field("home_work") List homework; + } + + static class ScoresWrapper { + + Scores scores; + ScoresWithMappedField scoresWithMappedField; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java new file mode 100644 index 000000000..25b4aed7c --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019 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.core.aggregation; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.bson.Document; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +public class UnsetOperationUnitTests { + + @Test // DATAMONGO-2331 + public void raisesErrorOnNullField() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new UnsetOperation(null)); + } + + @Test // DATAMONGO-2331 + public void rendersSingleFieldReferenceCorrectly() { + + assertThat(new UnsetOperation(Collections.singletonList("title")).toPipelineStages(contextFor(Book.class))) + .containsExactly(Document.parse("{\"$unset\" : \"title\" }")); + } + + @Test // DATAMONGO-2331 + public void rendersSingleMappedFieldReferenceCorrectly() { + + assertThat(new UnsetOperation(Collections.singletonList("stock")).toPipelineStages(contextFor(Book.class))) + .containsExactly(Document.parse("{\"$unset\" : \"copies\" }")); + } + + @Test // DATAMONGO-2331 + public void rendersSingleNestedMappedFieldReferenceCorrectly() { + + assertThat( + new UnsetOperation(Collections.singletonList("author.firstname")).toPipelineStages(contextFor(Book.class))) + .containsExactly(Document.parse("{\"$unset\" : \"author.first\"}")); + } + + @Test // DATAMONGO-2331 + public void rendersMultipleFieldReferencesCorrectly() { + + assertThat(new UnsetOperation(Arrays.asList("title", "author.firstname", "stock.location")) + .toPipelineStages(contextFor(Book.class))) + .containsExactly(Document.parse("{\"$unset\" : [\"title\", \"author.first\", \"copies.warehouse\"] }")); + } + + @Test // DATAMONGO-2331 + public void exposesFieldsCorrectly() { + assertThat(UnsetOperation.unset("title").and("isbn").getFields()).isEqualTo(ExposedFields.from()); + } + + private static AggregationOperationContext contextFor(@Nullable Class type) { + + if (type == null) { + return Aggregation.DEFAULT_CONTEXT; + } + + MappingMongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, + new MongoMappingContext()); + mongoConverter.afterPropertiesSet(); + + return new TypeBasedAggregationOperationContext(type, mongoConverter.getMappingContext(), + new QueryMapper(mongoConverter)); + } + + static class Book { + + @Id Integer id; + String title; + String isbn; + Author author; + @Field("copies") Collection stock; + } + + static class Author { + + @Field("first") String firstname; + @Field("last") String lastname; + } + + static class Warehouse { + + @Field("warehouse") String location; + Integer qty; + } +} diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index aa234b6f0..96b7495fd 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -996,6 +996,61 @@ assertThat(p.getFirstName(), is("Mary")); assertThat(p.getAge(), is(1)); ---- +[[mongo-template.aggregation-update]] +=== Aggregation Pipeline Updates + +The update methods exposed by `MongoOperations` and `ReactiveMongoOperations` also accept an <> via `AggregationUpdate`. +This allows to leverage https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline[MongoDB 4.2 aggregations] in an update operation. + +The update can consist of the following stages: + +* `AggregationUpdate.set(...).toValue(...)` -> `$set : { ... }` +* `AggregationUpdate.unset(...)` -> `$unset : [ ... ]` +* `AggregationUpdate.replaceWith(...)` -> `$replaceWith : { ... }` + +.Update Aggregation +==== +[source,java] +---- +AggregationUpdate update = Aggregation.newUpdate() + .set("average").toValue(ArithmeticOperators.valueOf("tests").avg()) <1> + .set("grade").toValue(ConditionalOperators.switchCases( <2> + when(valueOf("average").greaterThanEqualToValue(90)).then("A"), + when(valueOf("average").greaterThanEqualToValue(80)).then("B"), + when(valueOf("average").greaterThanEqualToValue(70)).then("C"), + when(valueOf("average").greaterThanEqualToValue(60)).then("D")) + .defaultTo("F") + ); + +template.update(Student.class) <3> + .apply(update) + .all(); <4> +---- +[source,javascript] +---- +db.students.update( <3> + { }, + [ + { $set: { average : { $avg: "$tests" } } }, <1> + { $set: { grade: { $switch: { <2> + branches: [ + { case: { $gte: [ "$average", 90 ] }, then: "A" }, + { case: { $gte: [ "$average", 80 ] }, then: "B" }, + { case: { $gte: [ "$average", 70 ] }, then: "C" }, + { case: { $gte: [ "$average", 60 ] }, then: "D" } + ], + default: "F" + } } } } + ], + { multi: true } <4> +) +---- +<1> The 1st `$set` stage calculates a new field _average_ based on the average of the _tests_ field. +<2> The 2nd `$set` stage calculates a new field _grade_ based on the _average_ field calculated by the first aggregation stage. +<3> The pipeline is executed on the _students_ collection and uses `Student` for the aggregation field mapping. +<4> Apply the update to all documents within the collection. +==== + [[mongo-template.find-and-replace]] === Finding and Replacing Documents