diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java
index 8951e34ec..501ff8652 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java
@@ -19,12 +19,12 @@ import java.util.Optional;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
-
-import com.mongodb.client.result.UpdateResult;
import org.springframework.lang.Nullable;
+import com.mongodb.client.result.UpdateResult;
+
/**
- * {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify operations in a
+ * {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify / findAndReplace operations in a
* fluent API style.
* The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, as well as
* the {@link Update} via {@code apply} into the MongoDB specific representations. The collection to operate on is by
@@ -73,6 +73,16 @@ public interface ExecutableUpdateOperation {
* @throws IllegalArgumentException if update is {@literal null}.
*/
TerminatingUpdate apply(Update update);
+
+ /**
+ * Specify {@code replacement} object.
+ *
+ * @param replacement must not be {@literal null}.
+ * @return new instance of {@link FindAndReplaceOptions}.
+ * @throws IllegalArgumentException if options is {@literal null}.
+ * 2.1
+ */
+ FindAndReplaceWithOptions replaceWith(T replacement);
}
/**
@@ -153,6 +163,47 @@ public interface ExecutableUpdateOperation {
T findAndModifyValue();
}
+ /**
+ * Define {@link FindAndReplaceOptions}.
+ *
+ * @author Mark Paluch
+ * @since 2.1
+ */
+ interface FindAndReplaceWithOptions extends TerminatingFindAndReplace {
+
+ /**
+ * Explicitly define {@link FindAndReplaceOptions} for the {@link Update}.
+ *
+ * @param options must not be {@literal null}.
+ * @return new instance of {@link FindAndReplaceOptions}.
+ * @throws IllegalArgumentException if options is {@literal null}.
+ */
+ TerminatingFindAndReplace withOptions(FindAndReplaceOptions options);
+ }
+
+ /**
+ * Trigger findAndReplace execution by calling one of the terminating methods.
+ */
+ interface TerminatingFindAndReplace {
+
+ /**
+ * Find, replace and return the first matching document.
+ *
+ * @return {@link Optional#empty()} if nothing found.
+ */
+ default Optional findAndReplace() {
+ return Optional.ofNullable(findAndReplaceValue());
+ }
+
+ /**
+ * Find, replace and return the first matching document.
+ *
+ * @return {@literal null} if nothing found.
+ */
+ @Nullable
+ T findAndReplaceValue();
+ }
+
/**
* Trigger update execution by calling one of the terminating methods.
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java
index fb3041ca8..c56579182 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java
@@ -51,7 +51,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.notNull(domainType, "DomainType must not be null!");
- return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null);
+ return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null);
}
/**
@@ -60,15 +60,17 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
*/
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
- static class ExecutableUpdateSupport
- implements ExecutableUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate {
+ static class ExecutableUpdateSupport implements ExecutableUpdate, UpdateWithCollection, UpdateWithQuery,
+ TerminatingUpdate, FindAndReplaceWithOptions, TerminatingFindAndReplace {
@NonNull MongoTemplate template;
@NonNull Class domainType;
Query query;
@Nullable Update update;
@Nullable String collection;
- @Nullable FindAndModifyOptions options;
+ @Nullable FindAndModifyOptions findAndModifyOptions;
+ @Nullable FindAndReplaceOptions findAndReplaceOptions;
+ @Nullable T replacement;
/*
* (non-Javadoc)
@@ -79,7 +81,8 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.notNull(update, "Update must not be null!");
- return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options);
+ return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+ findAndReplaceOptions, replacement);
}
/*
@@ -91,7 +94,8 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.hasText(collection, "Collection must not be null nor empty!");
- return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options);
+ return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+ findAndReplaceOptions, replacement);
}
/*
@@ -103,7 +107,34 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.notNull(options, "Options must not be null!");
- return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options);
+ return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options,
+ findAndReplaceOptions, replacement);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithUpdate#replaceWith(Object)
+ */
+ @Override
+ public FindAndReplaceWithOptions replaceWith(T replacement) {
+
+ Assert.notNull(replacement, "Replacement must not be null!");
+
+ return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+ findAndReplaceOptions, replacement);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.FindAndReplaceWithOptions#withOptions(org.springframework.data.mongodb.core.FindAndReplaceOptions)
+ */
+ @Override
+ public TerminatingFindAndReplace withOptions(FindAndReplaceOptions options) {
+
+ Assert.notNull(options, "Options must not be null!");
+
+ return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+ options, replacement);
}
/*
@@ -115,7 +146,8 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.notNull(query, "Query must not be null!");
- return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options);
+ return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+ findAndReplaceOptions, replacement);
}
/*
@@ -151,7 +183,20 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
*/
@Override
public @Nullable T findAndModifyValue() {
- return template.findAndModify(query, update, options != null ? options : new FindAndModifyOptions(), domainType, getCollectionName());
+ return template.findAndModify(query, update,
+ findAndModifyOptions != null ? findAndModifyOptions : new FindAndModifyOptions(), domainType,
+ getCollectionName());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.TerminatingFindAndReplace#findAndReplaceValue()
+ */
+ @Override
+ public @Nullable T findAndReplaceValue() {
+ return template.findAndReplace(query, replacement,
+ findAndReplaceOptions != null ? findAndReplaceOptions : new FindAndReplaceOptions(), domainType,
+ getCollectionName());
}
private UpdateResult doUpdate(boolean multi, boolean upsert) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java
new file mode 100644
index 000000000..bea09f74b
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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 java.util.Optional;
+
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.lang.Nullable;
+
+/**
+ * Options for
+ * findOneAndReplace.
+ *
+ * @author Mark Paluch
+ * @since 2.1
+ */
+public class FindAndReplaceOptions {
+
+ private boolean returnNew;
+ private boolean upsert;
+
+ private @Nullable Collation collation;
+
+ /**
+ * Static factory method to create a {@link FindAndReplaceOptions} instance.
+ *
+ * @return a new instance
+ */
+ public static FindAndReplaceOptions options() {
+ return new FindAndReplaceOptions();
+ }
+
+ /**
+ * @param options
+ * @return
+ */
+ public static FindAndReplaceOptions of(@Nullable FindAndReplaceOptions source) {
+
+ FindAndReplaceOptions options = new FindAndReplaceOptions();
+
+ if (source == null) {
+ return options;
+ }
+
+ options.returnNew = source.returnNew;
+ options.upsert = source.upsert;
+ options.collation = source.collation;
+
+ return options;
+ }
+
+ public FindAndReplaceOptions returnNew(boolean returnNew) {
+ this.returnNew = returnNew;
+ return this;
+ }
+
+ public FindAndReplaceOptions upsert(boolean upsert) {
+ this.upsert = upsert;
+ return this;
+ }
+
+ /**
+ * Define the {@link Collation} specifying language-specific rules for string comparison.
+ *
+ * @param collation
+ * @return
+ */
+ public FindAndReplaceOptions collation(@Nullable Collation collation) {
+
+ this.collation = collation;
+ return this;
+ }
+
+ public boolean isReturnNew() {
+ return returnNew;
+ }
+
+ public boolean isUpsert() {
+ return upsert;
+ }
+
+ /**
+ * Get the {@link Collation} specifying language-specific rules for string comparison.
+ *
+ * @return
+ */
+ public Optional getCollation() {
+ return Optional.ofNullable(collation);
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
index 3f3394104..c06a2998e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
@@ -894,6 +894,98 @@ public interface MongoOperations extends FluentMongoOperations {
T findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass,
String collectionName);
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
+ * document.
+ *
+ * @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 replacement the replacement document. Must not be {@literal null}.
+ * @return the converted object that was updated or {@literal null}, if not found.
+ * @since 2.1
+ */
+ @Nullable
+ default T findAndReplace(Query query, T replacement) {
+ return findAndReplace(query, replacement, FindAndReplaceOptions.options());
+ }
+
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
+ * document.
+ *
+ * @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 replacement the replacement document. 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.
+ * @since 2.1
+ */
+ @Nullable
+ default T findAndReplace(Query query, T replacement, String collectionName) {
+ return findAndReplace(query, replacement, FindAndReplaceOptions.options(), collectionName);
+ }
+
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
+ * taking {@link FindAndReplaceOptions} 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 replacement the replacement document. Must not be {@literal null}.
+ * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
+ * @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
+ * {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
+ * as it is after the update.
+ * @since 2.1
+ */
+ @Nullable
+ T findAndReplace(Query query, T replacement, FindAndReplaceOptions options);
+
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
+ * taking {@link FindAndReplaceOptions} 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 replacement the replacement document. Must not be {@literal null}.
+ * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
+ * @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
+ * {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
+ * as it is after the update.
+ * @since 2.1
+ */
+ @Nullable
+ T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName);
+
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
+ * taking {@link FindAndReplaceOptions} 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 replacement the replacement document. 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 FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
+ * as it is after the update.
+ * @since 2.1
+ */
+ @Nullable
+ T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityClass,
+ String collectionName);
+
/**
* Map the results of an ad-hoc query on the collection for the entity type to a single instance of an object of the
* specified type. The first document that matches the query is returned and also removed from the collection in the
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
index 5a5f88f91..4ae295797 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
@@ -1070,6 +1070,64 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
getMappedSortObject(query, entityClass), entityClass, update, optionsToUse);
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.MongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions)
+ */
+ @Override
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
+
+ Assert.notNull(replacement, "Replacement must not be null!");
+
+ Class entityClass = (Class) ClassUtils.getUserClass(replacement);
+ String collectionName = determineCollectionName(entityClass);
+
+ return findAndReplace(query, replacement, options, collectionName);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.MongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions, java.lang.String)
+ */
+ @Override
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
+
+ Assert.notNull(replacement, "Replacement must not be null!");
+
+ Class entityClass = (Class) ClassUtils.getUserClass(replacement);
+
+ return findAndReplace(query, replacement, options, entityClass, collectionName);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.MongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions, java.lang.Class, java.lang.String)
+ */
+ @Override
+ public T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityClass,
+ String collectionName) {
+
+ Assert.notNull(query, "Query must not be null!");
+ Assert.notNull(replacement, "Replacement must not be null!");
+ Assert.notNull(options, "Options must not be null!");
+ Assert.notNull(entityClass, "Entity class must not be null!");
+ Assert.notNull(collectionName, "CollectionName must not be null!");
+
+ FindAndReplaceOptions optionsToUse = FindAndReplaceOptions.of(options);
+
+ Optionals.ifAllPresent(query.getCollation(), optionsToUse.getCollation(), (l, r) -> {
+ throw new IllegalArgumentException(
+ "Both Query and FindAndReplaceOptions define a collation. Please provide the collation only via one of the two.");
+ });
+
+ query.getCollation().ifPresent(optionsToUse::collation);
+
+ return doFindAndReplace(collectionName, query.getQueryObject(), query.getFieldsObject(), query.getSortObject(),
+ entityClass, replacement, options);
+ }
+
// Find methods that take a Query to express the query and that return a single object that is also removed from the
// collection in the database.
@@ -2603,6 +2661,32 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
new ReadDocumentCallback(readerToUse, entityClass, collectionName), collectionName);
}
+ protected T doFindAndReplace(String collectionName, Document query, Document fields, Document sort,
+ Class entityClass, Object replacement, @Nullable FindAndReplaceOptions options) {
+
+ EntityReader super T, Bson> readerToUse = this.mongoConverter;
+
+ if (options == null) {
+ options = new FindAndReplaceOptions();
+ }
+
+ MongoPersistentEntity> entity = mappingContext.getPersistentEntity(entityClass);
+
+ Document mappedQuery = queryMapper.getMappedObject(query, entity);
+ Document dbDoc = toDocument(replacement, this.mongoConverter);
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "findAndReplace using query: {} fields: {} sort: {} for class: {} and replacement: {} " + "in collection: {}",
+ serializeToJsonSafely(mappedQuery), fields, sort, entityClass, serializeToJsonSafely(dbDoc), collectionName);
+ }
+
+ maybeEmitEvent(new BeforeSaveEvent<>(replacement, dbDoc, collectionName));
+
+ return executeFindOneInternal(new FindAndReplaceCallback(mappedQuery, fields, sort, dbDoc, options),
+ new ReadDocumentCallback(readerToUse, entityClass, collectionName), collectionName);
+ }
+
/**
* Populates the id property of the saved object, if it's not set already.
*
@@ -3018,6 +3102,41 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
}
+ private static class FindAndReplaceCallback implements CollectionCallback {
+
+ private final Document query;
+ private final Document fields;
+ private final Document sort;
+ private final Document update;
+ private final FindAndReplaceOptions options;
+
+ public FindAndReplaceCallback(Document query, Document fields, Document sort, Document update,
+ FindAndReplaceOptions options) {
+ this.query = query;
+ this.fields = fields;
+ this.sort = sort;
+ this.update = update;
+ this.options = options;
+ }
+
+ public Document doInCollection(MongoCollection collection) throws MongoException, DataAccessException {
+
+ FindOneAndReplaceOptions opts = new FindOneAndReplaceOptions();
+ opts.sort(sort);
+ if (options.isUpsert()) {
+ opts.upsert(true);
+ }
+ opts.projection(fields);
+ if (options.isReturnNew()) {
+ opts.returnDocument(ReturnDocument.AFTER);
+ }
+
+ options.getCollation().map(Collation::toMongoCollation).ifPresent(opts::collation);
+
+ return collection.findOneAndReplace(query, update, opts);
+ }
+ }
+
/**
* Simple internal callback to allow operations on a {@link Document}.
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
index c16e18248..de5844d5b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
@@ -688,6 +688,93 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
Mono findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass,
String collectionName);
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
+ * document.
+ *
+ * @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 replacement the replacement document. Must not be {@literal null}.
+ * @return the converted object that was updated or {@link Mono#empty()}, if not found.
+ * @since 2.1
+ */
+ default Mono findAndReplace(Query query, T replacement) {
+ return findAndReplace(query, replacement, FindAndReplaceOptions.options());
+ }
+
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
+ * document.
+ *
+ * @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 replacement the replacement document. Must not be {@literal null}.
+ * @param collectionName the collection to query. Must not be {@literal null}.
+ * @return the converted object that was updated or {@link Mono#empty()}, if not found.
+ * @since 2.1
+ */
+ default Mono findAndReplace(Query query, T replacement, String collectionName) {
+ return findAndReplace(query, replacement, FindAndReplaceOptions.options(), collectionName);
+ }
+
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
+ * taking {@link FindAndReplaceOptions} 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 replacement the replacement document. Must not be {@literal null}.
+ * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
+ * @return the converted object that was updated or {@link Mono#empty()}, if not found. Depending on the value of
+ * {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
+ * as it is after the update.
+ * @since 2.1
+ */
+ Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options);
+
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
+ * taking {@link FindAndReplaceOptions} 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 replacement the replacement document. Must not be {@literal null}.
+ * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
+ * @return the converted object that was updated or {@link Mono#empty()}, if not found. Depending on the value of
+ * {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
+ * as it is after the update.
+ * @since 2.1
+ */
+ Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName);
+
+ /**
+ * Triggers
+ * findOneAndReplace
+ * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
+ * taking {@link FindAndReplaceOptions} 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 replacement the replacement document. 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 {@link Mono#empty()}, if not found. Depending on the value of
+ * {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
+ * as it is after the update.
+ * @since 2.1
+ */
+ Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityClass,
+ String collectionName);
+
/**
* Map the results of an ad-hoc query on the collection for the entity type to a single instance of an object of the
* specified type. The first document that matches the query is returned and also removed from the collection in the
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
index 82052d5fb..40fd2ed91 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
@@ -129,6 +129,7 @@ import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.FindOneAndDeleteOptions;
+import com.mongodb.client.model.FindOneAndReplaceOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.ReturnDocument;
@@ -1108,6 +1109,63 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
getMappedSortObject(query, entityClass), entityClass, update, optionsToUse);
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
+
+ Assert.notNull(replacement, "Replacement must not be null!");
+
+ Class entityClass = (Class) ClassUtils.getUserClass(replacement);
+ String collectionName = determineCollectionName(entityClass);
+
+ return findAndReplace(query, replacement, options, collectionName);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions, java.lang.String)
+ */
+ @Override
+ public Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
+
+ Assert.notNull(replacement, "Replacement must not be null!");
+
+ Class entityClass = (Class) ClassUtils.getUserClass(replacement);
+
+ return findAndReplace(query, replacement, options, entityClass, collectionName);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions, java.lang.Class, java.lang.String)
+ */
+ @Override
+ public Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityClass,
+ String collectionName) {
+
+ Assert.notNull(query, "Query must not be null!");
+ Assert.notNull(replacement, "Replacement must not be null!");
+ Assert.notNull(options, "Options must not be null!");
+ Assert.notNull(entityClass, "Entity class must not be null!");
+ Assert.notNull(collectionName, "CollectionName must not be null!");
+
+ FindAndReplaceOptions optionsToUse = FindAndReplaceOptions.of(options);
+
+ Optionals.ifAllPresent(query.getCollation(), optionsToUse.getCollation(), (l, r) -> {
+ throw new IllegalArgumentException(
+ "Both Query and FindAndReplaceOptions define a collation. Please provide the collation only via one of the two.");
+ });
+
+ query.getCollation().ifPresent(optionsToUse::collation);
+
+ return doFindAndReplace(collectionName, query.getQueryObject(), query.getFieldsObject(), query.getSortObject(),
+ entityClass, replacement, options);
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndRemove(org.springframework.data.mongodb.core.query.Query, java.lang.Class)
@@ -2453,6 +2511,31 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
});
}
+ protected Mono doFindAndReplace(String collectionName, Document query, Document fields, Document sort,
+ Class entityClass, Object replacement, @Nullable FindAndReplaceOptions options) {
+
+ MongoPersistentEntity> entity = mappingContext.getPersistentEntity(entityClass);
+
+ return Mono.defer(() -> {
+
+ Document mappedQuery = queryMapper.getMappedObject(query, entity);
+ Document dbDoc = toDbObject(replacement, this.mongoConverter);
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "findAndReplace using query: {} fields: {} sort: {} for class: {} and replacement: {} "
+ + "in collection: {}",
+ serializeToJsonSafely(mappedQuery), fields, sort, entityClass, serializeToJsonSafely(dbDoc),
+ collectionName);
+ }
+
+ maybeEmitEvent(new BeforeSaveEvent<>(replacement, dbDoc, collectionName));
+
+ return executeFindOneInternal(new FindAndReplaceCallback(mappedQuery, fields, sort, dbDoc, options),
+ new ReadDocumentCallback(this.mongoConverter, entityClass, collectionName), collectionName);
+ });
+ }
+
protected void maybeEmitEvent(MongoMappingEvent event) {
if (null != eventPublisher) {
eventPublisher.publishEvent(event);
@@ -2941,6 +3024,45 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
}
+ /**
+ * @author Mark Paluch
+ */
+ @RequiredArgsConstructor
+ private static class FindAndReplaceCallback implements ReactiveCollectionCallback {
+
+ private final Document query;
+ private final Document fields;
+ private final Document sort;
+ private final Document update;
+ private final FindAndReplaceOptions options;
+
+ @Override
+ public Publisher doInCollection(MongoCollection collection)
+ throws MongoException, DataAccessException {
+
+ FindOneAndReplaceOptions findOneAndReplaceOptions = convertToFindOneAndReplaceOptions(options, fields, sort);
+ return collection.findOneAndReplace(query, update, findOneAndReplaceOptions);
+ }
+
+ private FindOneAndReplaceOptions convertToFindOneAndReplaceOptions(FindAndReplaceOptions options, Document fields,
+ Document sort) {
+
+ FindOneAndReplaceOptions result = new FindOneAndReplaceOptions();
+
+ result = result.projection(fields).sort(sort).upsert(options.isUpsert());
+
+ if (options.isReturnNew()) {
+ result = result.returnDocument(ReturnDocument.AFTER);
+ } else {
+ result = result.returnDocument(ReturnDocument.BEFORE);
+ }
+
+ result = options.getCollation().map(Collation::toMongoCollation).map(result::collation).orElse(result);
+
+ return result;
+ }
+ }
+
private static FindOneAndDeleteOptions convertToFindOneAndDeleteOptions(Document fields, Document sort) {
FindOneAndDeleteOptions result = new FindOneAndDeleteOptions();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java
index dce516b41..002beb366 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java
@@ -18,12 +18,13 @@ package org.springframework.data.mongodb.core;
import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
import com.mongodb.client.result.UpdateResult;
/**
- * {@link ReactiveUpdateOperation} allows creation and execution of reactive MongoDB update / findAndModify operations
- * in a fluent API style.
+ * {@link ReactiveUpdateOperation} allows creation and execution of reactive MongoDB update / findAndModify /
+ * findAndReplace operations in a fluent API style.
* The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, as well as
* the {@link org.springframework.data.mongodb.core.query.Update} via {@code apply} into the MongoDB specific
* representations. The collection to operate on is by default derived from the initial {@literal domainType} and can be
@@ -68,6 +69,21 @@ public interface ReactiveUpdateOperation {
Mono findAndModify();
}
+ /**
+ * Compose findAndReplace execution by calling one of the terminating methods.
+ *
+ * @since 2.1
+ */
+ interface TerminatingFindAndReplace {
+
+ /**
+ * Find, replace and return the first matching document.
+ *
+ * @return {@link Mono#empty()} if nothing found. Never {@literal null}.
+ */
+ Mono findAndReplace();
+ }
+
/**
* Compose update execution by calling one of the terminating methods.
*/
@@ -108,6 +124,16 @@ public interface ReactiveUpdateOperation {
* @throws IllegalArgumentException if update is {@literal null}.
*/
TerminatingUpdate apply(org.springframework.data.mongodb.core.query.Update update);
+
+ /**
+ * Specify {@code replacement} object.
+ *
+ * @param replacement must not be {@literal null}.
+ * @return new instance of {@link FindAndReplaceOptions}.
+ * @throws IllegalArgumentException if options is {@literal null}.
+ * @since 2.1
+ */
+ FindAndReplaceWithOptions replaceWith(T replacement);
}
/**
@@ -157,5 +183,23 @@ public interface ReactiveUpdateOperation {
TerminatingFindAndModify withOptions(FindAndModifyOptions options);
}
+ /**
+ * Define {@link FindAndReplaceOptions}.
+ *
+ * @author Mark Paluch
+ * @since 2.1
+ */
+ interface FindAndReplaceWithOptions extends TerminatingFindAndReplace {
+
+ /**
+ * Explicitly define {@link FindAndReplaceOptions} for the {@link Update}.
+ *
+ * @param options must not be {@literal null}.
+ * @return new instance of {@link FindAndReplaceOptions}.
+ * @throws IllegalArgumentException if options is {@literal null}.
+ */
+ TerminatingFindAndReplace withOptions(FindAndReplaceOptions options);
+ }
+
interface ReactiveUpdate extends UpdateWithCollection, UpdateWithQuery, UpdateWithUpdate {}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java
index 5b7b02421..11feea58a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java
@@ -22,6 +22,7 @@ import lombok.experimental.FieldDefaults;
import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -50,20 +51,22 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(domainType, "DomainType must not be null!");
- return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null);
+ return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null);
}
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
- static class ReactiveUpdateSupport
- implements ReactiveUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate {
+ static class ReactiveUpdateSupport implements ReactiveUpdate, UpdateWithCollection, UpdateWithQuery,
+ TerminatingUpdate, FindAndReplaceWithOptions, TerminatingFindAndReplace {
@NonNull ReactiveMongoTemplate template;
@NonNull Class domainType;
Query query;
org.springframework.data.mongodb.core.query.Update update;
- String collection;
- FindAndModifyOptions options;
+ @Nullable String collection;
+ @Nullable FindAndModifyOptions findAndModifyOptions;
+ @Nullable FindAndReplaceOptions findAndReplaceOptions;
+ @Nullable T replacement;
/*
* (non-Javadoc)
@@ -74,7 +77,8 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(update, "Update must not be null!");
- return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options);
+ return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+ findAndReplaceOptions, replacement);
}
/*
@@ -86,7 +90,8 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.hasText(collection, "Collection must not be null nor empty!");
- return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options);
+ return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+ findAndReplaceOptions, replacement);
}
/*
@@ -116,7 +121,18 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
String collectionName = getCollectionName();
- return template.findAndModify(query, update, options, domainType, collectionName);
+ return template.findAndModify(query, update, findAndModifyOptions, domainType, collectionName);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.TerminatingFindAndReplace#findAndReplace()
+ */
+ @Override
+ public Mono findAndReplace() {
+ return template.findAndReplace(query, replacement,
+ findAndReplaceOptions != null ? findAndReplaceOptions : new FindAndReplaceOptions(), domainType,
+ getCollectionName());
}
/*
@@ -128,7 +144,8 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(query, "Query must not be null!");
- return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options);
+ return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+ findAndReplaceOptions, replacement);
}
/*
@@ -149,7 +166,34 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(options, "Options must not be null!");
- return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options);
+ return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options,
+ findAndReplaceOptions, replacement);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithUpdate#replaceWith(java.lang.Object)
+ */
+ @Override
+ public FindAndReplaceWithOptions replaceWith(T replacement) {
+
+ Assert.notNull(replacement, "Replacement must not be null!");
+
+ return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+ findAndReplaceOptions, replacement);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.FindAndReplaceWithOptions#withOptions(org.springframework.data.mongodb.core.FindAndReplaceOptions)
+ */
+ @Override
+ public TerminatingFindAndReplace withOptions(FindAndReplaceOptions options) {
+
+ Assert.notNull(options, "Options must not be null!");
+
+ return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, options,
+ replacement);
}
private Mono doUpdate(boolean multi, boolean upsert) {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java
index b6abc1816..fd8b6adac 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java
@@ -179,6 +179,58 @@ public class ExecutableUpdateOperationSupportTests {
assertThat(result.getUpsertedId()).isEqualTo(new BsonString("id-3"));
}
+ @Test // DATAMONGO-1827
+ public void findAndReplaceValue() {
+
+ Person luke = new Person();
+ luke.firstname = "Luke";
+
+ Person result = template.update(Person.class).matching(queryHan()).replaceWith(luke).findAndReplaceValue();
+
+ assertThat(result).isEqualTo(han);
+ assertThat(template.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname",
+ "Luke");
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplace() {
+
+ Person luke = new Person();
+ luke.firstname = "Luke";
+
+ Optional result = template.update(Person.class).matching(queryHan()).replaceWith(luke).findAndReplace();
+
+ assertThat(result).contains(han);
+ assertThat(template.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname",
+ "Luke");
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceWithCollection() {
+
+ Person luke = new Person();
+ luke.firstname = "Luke";
+
+ Optional result = template.update(Person.class).inCollection(STAR_WARS).matching(queryHan())
+ .replaceWith(luke).findAndReplace();
+
+ assertThat(result).contains(han);
+ assertThat(template.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname",
+ "Luke");
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceWithOptions() {
+
+ Person luke = new Person();
+ luke.firstname = "Luke";
+
+ Person result = template.update(Person.class).matching(queryHan()).replaceWith(luke)
+ .withOptions(FindAndReplaceOptions.options().returnNew(true)).findAndReplaceValue();
+
+ assertThat(result).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Luke");
+ }
+
private Query queryHan() {
return query(where("id").is(han.getId()));
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
index a7fe1777b..368d194f5 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
@@ -78,6 +78,7 @@ import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventLis
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
@@ -197,6 +198,7 @@ public class MongoTemplateTests {
template.dropCollection(TypeWithDate.class);
template.dropCollection("collection");
template.dropCollection("personX");
+ template.dropCollection("findandreplace");
template.dropCollection(Document.class);
template.dropCollection(ObjectWith3AliasedFields.class);
template.dropCollection(ObjectWith3AliasedFieldsAndNestedAddress.class);
@@ -2374,6 +2376,54 @@ public class MongoTemplateTests {
assertThat(retrieved.models.get(0).get(1).value(), equalTo("value2"));
}
+ @Test // DATAMONGO-1827
+ public void findAndReplaceShouldReplaceDocument() {
+
+ org.bson.Document doc = new org.bson.Document("foo", "bar");
+ template.save(doc, "findandreplace");
+
+ org.bson.Document replacement = new org.bson.Document("foo", "baz");
+ org.bson.Document previous = template.findAndReplace(query(where("foo").is("bar")), replacement,
+ FindAndReplaceOptions.options(), org.bson.Document.class, "findandreplace");
+
+ assertThat(previous).containsEntry("foo", "bar");
+ assertThat(template.findOne(query(where("foo").is("baz")), org.bson.Document.class, "findandreplace")).isNotNull();
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceShouldReplaceObject() {
+
+ MyPerson person = new MyPerson("Walter");
+ template.save(person);
+
+ MyPerson previous = template.findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg"));
+
+ assertThat(previous.getName()).isEqualTo("Walter");
+ assertThat(template.findOne(query(where("name").is("Heisenberg")), MyPerson.class)).isNotNull();
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceShouldReplaceObjectReturingNew() {
+
+ MyPerson person = new MyPerson("Walter");
+ template.save(person);
+
+ MyPerson updated = template.findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg"),
+ FindAndReplaceOptions.options().returnNew(true));
+
+ assertThat(updated.getName()).isEqualTo("Heisenberg");
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceShouldFailWithTwoCollationObjects() {
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Both Query and FindAndReplaceOptions");
+
+ template.findAndReplace(query(where("name").is("Walter")).collation(Collation.of("de")), new MyPerson("Heisenberg"),
+ FindAndReplaceOptions.options().collation(Collation.of("en")));
+ }
+
@Test // DATAMONGO-407
public void updatesShouldRetainTypeInformationEvenForCollections() {
@@ -3608,6 +3658,12 @@ public class MongoTemplateTests {
String name;
Address address;
+ public MyPerson() {}
+
+ public MyPerson(String name) {
+ this.name = name;
+ }
+
public String getName() {
return name;
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java
index df20fa83c..3beef8ee0 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java
@@ -58,12 +58,14 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.geo.Metrics;
import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mongodb.core.MongoTemplateTests.Address;
import org.springframework.data.mongodb.core.MongoTemplateTests.PersonWithConvertedId;
import org.springframework.data.mongodb.core.MongoTemplateTests.VersionedPerson;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
import org.springframework.data.mongodb.core.index.GeospatialIndex;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexOperationsAdapter;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
@@ -101,7 +103,8 @@ public class ReactiveMongoTemplateTests {
.mergeWith(template.dropCollection(PersonWithAList.class)) //
.mergeWith(template.dropCollection(PersonWithIdPropertyOfTypeObjectId.class)) //
.mergeWith(template.dropCollection(PersonWithVersionPropertyOfTypeInteger.class)) //
- .mergeWith(template.dropCollection(Sample.class))) //
+ .mergeWith(template.dropCollection(Sample.class)) //
+ .mergeWith(template.dropCollection(MyPerson.class))) //
.verifyComplete();
}
@@ -453,6 +456,68 @@ public class ReactiveMongoTemplateTests {
assertThat(p.getAge(), is(1));
}
+ @Test // DATAMONGO-1827
+ public void findAndReplaceShouldReplaceDocument() {
+
+ org.bson.Document doc = new org.bson.Document("foo", "bar");
+ template.save(doc, "findandreplace").as(StepVerifier::create).expectNextCount(1).verifyComplete();
+
+ org.bson.Document replacement = new org.bson.Document("foo", "baz");
+ template
+ .findAndReplace(query(where("foo").is("bar")), replacement, FindAndReplaceOptions.options(),
+ org.bson.Document.class, "findandreplace") //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual, hasEntry("foo", "bar"));
+ }).verifyComplete();
+
+ template.findOne(query(where("foo").is("baz")), org.bson.Document.class, "findandreplace") //
+ .as(StepVerifier::create) //
+ .expectNextCount(1) //
+ .verifyComplete();
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceShouldReplaceObject() {
+
+ MyPerson person = new MyPerson("Walter");
+ template.save(person).as(StepVerifier::create).expectNextCount(1).verifyComplete();
+
+ template.findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg")) //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual.getName(), is("Walter"));
+ }).verifyComplete();
+
+ template.findOne(query(where("name").is("Heisenberg")), MyPerson.class) //
+ .as(StepVerifier::create).expectNextCount(1).verifyComplete();
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceShouldReplaceObjectReturingNew() {
+
+ MyPerson person = new MyPerson("Walter");
+ template.save(person).as(StepVerifier::create).expectNextCount(1).verifyComplete();
+
+ template
+ .findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg"),
+ FindAndReplaceOptions.options().returnNew(true))
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual.getName(), is("Heisenberg"));
+ }).verifyComplete();
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceShouldFailWithTwoCollationObjects() {
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Both Query and FindAndReplaceOptions");
+
+ template.findAndReplace(query(where("name").is("Walter")).collation(Collation.of("de")), new MyPerson("Heisenberg"),
+ FindAndReplaceOptions.options().collation(Collation.of("en")));
+ }
+
@Test // DATAMONGO-1444
public void testFindAllAndRemoveFullyReturnsAndRemovesDocuments() {
@@ -1178,4 +1243,21 @@ public class ReactiveMongoTemplateTests {
this.field = field;
}
}
+
+ public static class MyPerson {
+
+ String id;
+ String name;
+ Address address;
+
+ public MyPerson() {}
+
+ public MyPerson(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java
index 3f4b9f83a..13ae3e971 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java
@@ -181,6 +181,52 @@ public class ReactiveUpdateOperationSupportTests {
}).verifyComplete();
}
+ @Test // DATAMONGO-1827
+ public void findAndReplace() {
+
+ Person luke = new Person();
+ luke.firstname = "Luke";
+
+ template.update(Person.class).matching(queryHan()).replaceWith(luke).findAndReplace() //
+ .as(StepVerifier::create).expectNext(han).verifyComplete();
+
+ template.findOne(queryHan(), Person.class) //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Luke");
+ }).verifyComplete();
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceWithCollection() {
+
+ Person luke = new Person();
+ luke.firstname = "Luke";
+
+ template.update(Person.class).inCollection(STAR_WARS).matching(queryHan()).replaceWith(luke).findAndReplace() //
+ .as(StepVerifier::create).expectNext(han).verifyComplete();
+
+ template.findOne(queryHan(), Person.class) //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Luke");
+ }).verifyComplete();
+ }
+
+ @Test // DATAMONGO-1827
+ public void findAndReplaceWithOptions() {
+
+ Person luke = new Person();
+ luke.firstname = "Luke";
+
+ template.update(Person.class).matching(queryHan()).replaceWith(luke)
+ .withOptions(FindAndReplaceOptions.options().returnNew(true)).findAndReplace() //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Luke");
+ }).verifyComplete();
+ }
+
private Query queryHan() {
return query(where("id").is(han.getId()));
}
diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc
index d3ed101f3..592c18887 100644
--- a/src/main/asciidoc/new-features.adoc
+++ b/src/main/asciidoc/new-features.adoc
@@ -14,6 +14,7 @@
* <> support for the imperative and reactive Template APIs.
* <> support and a MongoDB-specific transaction manager implementation.
* <> using `@Query(sort=…)`.
+* `findAndReplace` support through imperative and reactive Template APIs.
[[new-features.2-0-0]]
== What's New in Spring Data MongoDB 2.0