DATAMONGO-2331 - Add support for Update with an aggregation pipeline.

Now the update methods exposed by (Reactive)MongoOperations also accept an Aggregation Pipeline via AggregationUpdate.

The update can consist of the following stages:

* AggregationUpdate.set(...).toValue(...) -> $set : { ... }
* AggregationUpdate.unset(...) -> $unset : [ ... ]
* AggregationUpdate.replaceWith(...) -> $replaceWith : { ... }

AggregationUpdate update = Aggregation.newUpdate()
    .set("average").toValue(ArithmeticOperators.valueOf("tests").avg())
    .set("grade").toValue(ConditionalOperators.switchCases(
        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)
    .apply(update)
    .all();

Original pull request: #789.
This commit is contained in:
Christoph Strobl
2019-09-17 08:31:55 +02:00
committed by Mark Paluch
parent 9eaf67148d
commit 32cbae0e5f
25 changed files with 2981 additions and 158 deletions

View File

@@ -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<T> {
/**
* 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<T> apply(Update update);
TerminatingUpdate<T> 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<T> apply(Update update) {
return apply((UpdateDefinition) update);
}
/**
* Specify {@code replacement} object.

View File

@@ -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<T> apply(Update update) {
public TerminatingUpdate<T> apply(UpdateDefinition update) {
Assert.notNull(update, "Update must not be null!");

View File

@@ -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).
* <p />
* <p/>
* <strong>NOTE:</strong> Some operations cannot be executed within a MongoDB transaction. Please refer to the MongoDB
* specific documentation to learn more about <a href="https://docs.mongodb.com/manual/core/transactions/">Multi
* Document Transactions</a>.
@@ -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.
* <p />
* <p/>
* <strong>Note:</strong> 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}.
* <p />
* <p/>
* <strong>Note:</strong> 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:
* </p>
*
*
* <pre class="code">
* TypedAggregation&lt;T&gt; 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:
* </p>
*
*
* <pre class="code">
* TypedAggregation&lt;T&gt; 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 <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* 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> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* 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> T findAndModify(Query query, Update update, Class<T> entityClass) {
return findAndModify(query, (UpdateDefinition) update, entityClass);
}
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* 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> T findAndModify(Query query, Update update, Class<T> entityClass);
<T> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass, String collectionName);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
@@ -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> T findAndModify(Query query, Update update, Class<T> entityClass, String collectionName) {
return findAndModify(query, (UpdateDefinition) update, entityClass, collectionName);
}
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* 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> T findAndModify(Query query, Update update, Class<T> entityClass, String collectionName);
<T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
@@ -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> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass);
@Deprecated
default <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass) {
return findAndModify(query, (UpdateDefinition) update, options, entityClass);
}
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* 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> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
@@ -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> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName);
default <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> 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. <br />
* <strong>NOTE:</strong> 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. <br />
* <strong>NOTE:</strong> 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. <br />
*
* @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. <br />
* <strong>NOTE:</strong> 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

View File

@@ -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> T findAndModify(Query query, Update update, Class<T> entityClass) {
public <T> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass) {
return findAndModify(query, update, new FindAndModifyOptions(), entityClass, getCollectionName(entityClass));
}
@Nullable
@Override
public <T> T findAndModify(Query query, Update update, Class<T> entityClass, String collectionName) {
public <T> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass, String collectionName) {
return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName);
}
@Nullable
@Override
public <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass) {
public <T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass) {
return findAndModify(query, update, options, entityClass, getCollectionName(entityClass));
}
@Nullable
@Override
public <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
public <T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> 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<Document> 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> T doFindAndModify(String collectionName, Document query, Document fields, Document sort,
Class<T> entityClass, Update update, @Nullable FindAndModifyOptions options) {
Class<T> entityClass, UpdateDefinition update, @Nullable FindAndModifyOptions options) {
EntityReader<? super T, Bson> 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<Document> 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<Document> 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<Document>) update, opts);
}
throw new IllegalArgumentException("doh - that does not work");
}
}

View File

@@ -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
<T> Flux<GeoResult<T>> geoNear(NearQuery near, Class<T> entityClass, String collectionName);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* 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
*/
<T> Mono<T> findAndModify(Query query, UpdateDefinition update, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* 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)}.
*/
<T> Mono<T> findAndModify(Query query, Update update, Class<T> entityClass);
@Deprecated
default <T> Mono<T> findAndModify(Query query, Update update, Class<T> entityClass) {
return findAndModify(query, (UpdateDefinition) update, entityClass);
}
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* 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
*/
<T> Mono<T> findAndModify(Query query, UpdateDefinition update, Class<T> entityClass, String collectionName);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
@@ -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)}.
*/
<T> Mono<T> findAndModify(Query query, Update update, Class<T> entityClass, String collectionName);
@Deprecated
default <T> Mono<T> findAndModify(Query query, Update update, Class<T> entityClass, String collectionName) {
return findAndModify(query, (UpdateDefinition) update, entityClass, collectionName);
}
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* 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
*/
<T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
@@ -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)}.
*/
<T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass);
@Deprecated
default <T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass) {
return findAndModify(query, (UpdateDefinition) update, options, entityClass);
}
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* 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
*/
<T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
@@ -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)}.
*/
<T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName);
@Deprecated
default <T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> 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<UpdateResult> upsert(Query query, Update update, Class<?> entityClass);
Mono<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 Mono<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
* combining the query document and the update document. <br />
* <strong>NOTE:</strong> 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<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
@@ -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<UpdateResult> upsert(Query query, Update update, String collectionName);
@Deprecated
default Mono<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
*/
Mono<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
@@ -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<UpdateResult> upsert(Query query, Update update, Class<?> entityClass, String collectionName);
@Deprecated
default Mono<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
@@ -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<UpdateResult> updateFirst(Query query, Update update, Class<?> entityClass);
Mono<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
* 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<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
@@ -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<UpdateResult> updateFirst(Query query, Update update, String collectionName);
Mono<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. <br />
* <strong>NOTE:</strong> 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<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
@@ -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<UpdateResult> updateFirst(Query query, Update update, Class<?> entityClass, String collectionName);
Mono<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
* the provided updated document. <br />
*
* @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<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
@@ -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<UpdateResult> updateMulti(Query query, Update update, Class<?> entityClass);
Mono<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 Mono<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
@@ -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<UpdateResult> updateMulti(Query query, Update update, String collectionName);
Mono<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. <br />
* <strong>NOTE:</strong> 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<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
@@ -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<UpdateResult> updateMulti(Query query, Update update, Class<?> entityClass, String collectionName);
Mono<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 Mono<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 id.

View File

@@ -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 <T> Mono<T> findAndModify(Query query, Update update, Class<T> entityClass) {
public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, Class<T> 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 <T> Mono<T> findAndModify(Query query, Update update, Class<T> entityClass, String collectionName) {
public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, Class<T> 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 <T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass) {
public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
Class<T> 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 <T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName) {
public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
Class<T> 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<UpdateResult> upsert(Query query, Update update, Class<?> entityClass) {
public Mono<UpdateResult> 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<UpdateResult> upsert(Query query, Update update, String collectionName) {
public Mono<UpdateResult> 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<UpdateResult> upsert(Query query, Update update, Class<?> entityClass, String collectionName) {
public Mono<UpdateResult> 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<UpdateResult> updateFirst(Query query, Update update, Class<?> entityClass) {
public Mono<UpdateResult> 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<UpdateResult> updateFirst(Query query, Update update, String collectionName) {
public Mono<UpdateResult> 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<UpdateResult> updateFirst(Query query, Update update, Class<?> entityClass, String collectionName) {
public Mono<UpdateResult> 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<UpdateResult> updateMulti(Query query, Update update, Class<?> entityClass) {
public Mono<UpdateResult> 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<UpdateResult> updateMulti(Query query, Update update, String collectionName) {
public Mono<UpdateResult> 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<UpdateResult> updateMulti(Query query, Update update, Class<?> entityClass, String collectionName) {
public Mono<UpdateResult> 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<UpdateResult> 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<Document> collectionToUse = prepareCollection(collection, writeConcernToUse);
Flux<UpdateResult> 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<Document> 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<Document> 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 <T> Mono<T> doFindAndModify(String collectionName, Document query, Document fields, Document sort,
Class<T> entityClass, Update update, FindAndModifyOptions options) {
Class<T> 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<Document> 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<Document>) update, findOneAndUpdateOptions);
}
return Flux.error(new IllegalArgumentException("doh - that does not work"));
}
private static FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOptions options,

View File

@@ -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<T> apply(org.springframework.data.mongodb.core.query.Update update);
TerminatingUpdate<T> 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<T> apply(org.springframework.data.mongodb.core.query.Update update) {
return apply((UpdateDefinition) update);
}
/**
* Specify {@code replacement} object.

View File

@@ -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<T> apply(org.springframework.data.mongodb.core.query.Update update) {
public TerminatingUpdate<T> apply(org.springframework.data.mongodb.core.query.UpdateDefinition update) {
Assert.notNull(update, "Update must not be null!");

View File

@@ -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<AggregationOperation> 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

View File

@@ -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).
*
* <pre class="code">
* 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"));
* </pre>
*
* The above sample is equivalent to the JSON update statement
*
* <pre class="code">
* 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 }
* )
* </pre>
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline">MongoDB
* Reference Documentation</a>
* @since 2.3
*/
public class AggregationUpdate extends Aggregation implements UpdateDefinition {
private boolean isolated = false;
private Set<String> 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<AggregationOperation> 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<AggregationOperation> 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 <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/set/">$set Aggregation Reference</a>
*/
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 <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/unset/">$unset Aggregation
* Reference</a>
*/
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 <strong>_id</strong> field.
*
* @param replaceWithOperation
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/replaceWith/">$replaceWith Aggregation
* Reference</a>
*/
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 <strong>multiple</strong> documents from yielding to other reads or writes
* once the first document is written. <br />
* 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<ArrayFilter> 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);
}
}

View File

@@ -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<? extends MongoPersistentEntity<?>, 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));
}
}
}

View File

@@ -33,7 +33,8 @@ import org.springframework.util.Assert;
* @author Mark Paluch
* @author Christoph Strobl
* @since 1.10
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/replaceRoot/">MongoDB Aggregation Framework: $replaceRoot</a>
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/replaceRoot/">MongoDB Aggregation
* Framework: $replaceRoot</a>
*/
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}.
*

View File

@@ -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. <br />
* 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 <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/replaceWith/">MongoDB Aggregation
* Framework: $replaceWith</a>
*/
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)));
}
}

View File

@@ -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.
*
* <pre class="code">
* SetOperation.set("totalHomework").toValue("A+").and().set("totalQuiz").toValue("B-")
* </pre>
*
* @author Christoph Strobl
* @since 2.3
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/set/">MongoDB Aggregation Framework:
* $set</a>
*/
public class SetOperation implements InheritsFieldsAggregationOperation {
private Map<Object, Object> 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<Object, Object> 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<Object, Object> 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<Object, Object> 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<Object, Object> valueMap;
private FieldAppender() {
this.valueMap = new LinkedHashMap<>();
}
private FieldAppender(Map<Object, Object> 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);
}
}
}

View File

@@ -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<MongoPersistentProperty> propertyPath = mappingContext
.getPersistentPropertyPath(field.getTarget(), type);

View File

@@ -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 <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/unset/">MongoDB Aggregation Framework:
* $unset</a>
*/
public class UnsetOperation implements InheritsFieldsAggregationOperation {
private final Collection<Object> fields;
/**
* Create new instance of {@link UnsetOperation}.
*
* @param fields must not be {@literal null}.
*/
public UnsetOperation(Collection<Object> 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<Object> 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<Object> 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<String> removedFieldNames() {
List<String> 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;
}
}

View File

@@ -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<List<Document>> 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<List<Document>> 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<List<Document>> 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<List<Document>> 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<List<Document>> 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;

View File

@@ -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<org.bson.Document> all(Class<?> type) {
return collection(type).find(new org.bson.Document()).into(new ArrayList<>());
}
private MongoCollection<org.bson.Document> collection(Class<?> type) {
return client.getDatabase(DB_NAME).getCollection(template.getCollectionName(type));
}
@Document("scores")
static class Score {
Integer id;
String student;
List<Integer> homework;
List<Integer> quiz;
Integer extraCredit;
public Score(Integer id, String student, List<Integer> homework, List<Integer> 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<Warehouse> 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;
}
}

View File

@@ -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<List<Document>> 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<List<Document>> 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<List<Document>> 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<List<Document>> 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<List<Document>> 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;

View File

@@ -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<org.bson.Document> all(Class<?> type) {
return Flux.from(collection(type).find(new org.bson.Document()));
}
private MongoCollection<org.bson.Document> collection(Class<?> type) {
return client.getDatabase(DB_NAME).getCollection(template.getCollectionName(type));
}
@Document("scores")
static class Score {
Integer id;
String student;
List<Integer> homework;
List<Integer> quiz;
Integer extraCredit;
public Score(Integer id, String student, List<Integer> homework, List<Integer> 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<Warehouse> 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;
}
}

View File

@@ -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")));
}
}

View File

@@ -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]} } " + "} }"));
}
}

View File

@@ -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<Integer> homework;
}
static class ScoresWithMappedField {
@Field("student_name") String student;
@Field("home_work") List<Integer> homework;
}
static class ScoresWrapper {
Scores scores;
ScoresWithMappedField scoresWithMappedField;
}
}

View File

@@ -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<Warehouse> stock;
}
static class Author {
@Field("first") String firstname;
@Field("last") String lastname;
}
static class Warehouse {
@Field("warehouse") String location;
Integer qty;
}
}

View File

@@ -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 <<mongo.aggregation, Aggregation Pipeline>> 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