DATAMONGO-1827 - Add support for findAndReplace.

We now support findAndReplace operations through the imperative and reactive Template API to find an object by a query and entirely replace it (except for the _id).

template.findAndReplace(query(where("name").is("Han")), new Person("Luke"))

template.update(Person.class).inCollection(STAR_WARS).matching(query(where("name").is("Han"))).replaceWith(luke).findAndReplace()

Original Pull Request: #569
This commit is contained in:
Mark Paluch
2018-06-07 17:19:37 +02:00
committed by Christoph Strobl
parent 56e61a2965
commit fa880f1c5c
14 changed files with 970 additions and 25 deletions

View File

@@ -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. <br />
* 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<T> 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<T> replaceWith(T replacement);
}
/**
@@ -153,6 +163,47 @@ public interface ExecutableUpdateOperation {
T findAndModifyValue();
}
/**
* Define {@link FindAndReplaceOptions}.
*
* @author Mark Paluch
* @since 2.1
*/
interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T> {
/**
* 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<T> withOptions(FindAndReplaceOptions options);
}
/**
* Trigger findAndReplace execution by calling one of the terminating methods.
*/
interface TerminatingFindAndReplace<T> {
/**
* Find, replace and return the first matching document.
*
* @return {@link Optional#empty()} if nothing found.
*/
default Optional<T> 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.
*

View File

@@ -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<T>
implements ExecutableUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T> {
static class ExecutableUpdateSupport<T> implements ExecutableUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>,
TerminatingUpdate<T>, FindAndReplaceWithOptions<T>, TerminatingFindAndReplace<T> {
@NonNull MongoTemplate template;
@NonNull Class<T> 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<T> 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<T> 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) {

View File

@@ -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
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>.
*
* @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<Collation> getCollation() {
return Optional.ofNullable(collation);
}
}

View File

@@ -894,6 +894,98 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName);
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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> T findAndReplace(Query query, T replacement) {
return findAndReplace(query, replacement, FindAndReplaceOptions.options());
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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> T findAndReplace(Query query, T replacement, String collectionName) {
return findAndReplace(query, replacement, FindAndReplaceOptions.options(), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options);
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName);
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> 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

View File

@@ -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> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
Assert.notNull(replacement, "Replacement must not be null!");
Class<T> 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> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
Assert.notNull(replacement, "Replacement must not be null!");
Class<T> 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> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> 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<T>(readerToUse, entityClass, collectionName), collectionName);
}
protected <T> T doFindAndReplace(String collectionName, Document query, Document fields, Document sort,
Class<T> 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<T>(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<Document> {
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<Document> 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}.
*

View File

@@ -688,6 +688,93 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName);
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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 <T> Mono<T> findAndReplace(Query query, T replacement) {
return findAndReplace(query, replacement, FindAndReplaceOptions.options());
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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 <T> Mono<T> findAndReplace(Query query, T replacement, String collectionName) {
return findAndReplace(query, replacement, FindAndReplaceOptions.options(), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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
*/
<T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options);
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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
*/
<T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName);
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* 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
*/
<T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> 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

View File

@@ -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 <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
Assert.notNull(replacement, "Replacement must not be null!");
Class<T> 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 <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
Assert.notNull(replacement, "Replacement must not be null!");
Class<T> 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 <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> 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 <T> Mono<T> doFindAndReplace(String collectionName, Document query, Document fields, Document sort,
Class<T> 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<T>(this.mongoConverter, entityClass, collectionName), collectionName);
});
}
protected <T> void maybeEmitEvent(MongoMappingEvent<T> 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<Document> {
private final Document query;
private final Document fields;
private final Document sort;
private final Document update;
private final FindAndReplaceOptions options;
@Override
public Publisher<Document> doInCollection(MongoCollection<Document> 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();

View File

@@ -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. <br />
* {@link ReactiveUpdateOperation} allows creation and execution of reactive MongoDB update / findAndModify /
* findAndReplace operations in a fluent API style. <br />
* 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<T> findAndModify();
}
/**
* Compose findAndReplace execution by calling one of the terminating methods.
*
* @since 2.1
*/
interface TerminatingFindAndReplace<T> {
/**
* Find, replace and return the first matching document.
*
* @return {@link Mono#empty()} if nothing found. Never {@literal null}.
*/
Mono<T> 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<T> 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<T> replaceWith(T replacement);
}
/**
@@ -157,5 +183,23 @@ public interface ReactiveUpdateOperation {
TerminatingFindAndModify<T> withOptions(FindAndModifyOptions options);
}
/**
* Define {@link FindAndReplaceOptions}.
*
* @author Mark Paluch
* @since 2.1
*/
interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T> {
/**
* 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<T> withOptions(FindAndReplaceOptions options);
}
interface ReactiveUpdate<T> extends UpdateWithCollection<T>, UpdateWithQuery<T>, UpdateWithUpdate<T> {}
}

View File

@@ -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<T>
implements ReactiveUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T> {
static class ReactiveUpdateSupport<T> implements ReactiveUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>,
TerminatingUpdate<T>, FindAndReplaceWithOptions<T>, TerminatingFindAndReplace<T> {
@NonNull ReactiveMongoTemplate template;
@NonNull Class<T> 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<T> 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<T> 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<T> withOptions(FindAndReplaceOptions options) {
Assert.notNull(options, "Options must not be null!");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, options,
replacement);
}
private Mono<UpdateResult> doUpdate(boolean multi, boolean upsert) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@
* <<mongo.sessions, MongoDB 3.6 Session>> support for the imperative and reactive Template APIs.
* <<mongo.transactions, MongoDB 4.0 Transaction>> support and a MongoDB-specific transaction manager implementation.
* <<mongodb.repositories.queries.sort,Default sort specifications for repository query methods>> 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