From fa880f1c5ca1588f16cf1c1e3350112e3c793c98 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 7 Jun 2018 17:19:37 +0200 Subject: [PATCH] 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 --- .../core/ExecutableUpdateOperation.java | 57 +++++++- .../ExecutableUpdateOperationSupport.java | 63 +++++++-- .../mongodb/core/FindAndReplaceOptions.java | 104 +++++++++++++++ .../data/mongodb/core/MongoOperations.java | 92 +++++++++++++ .../data/mongodb/core/MongoTemplate.java | 119 +++++++++++++++++ .../mongodb/core/ReactiveMongoOperations.java | 87 +++++++++++++ .../mongodb/core/ReactiveMongoTemplate.java | 122 ++++++++++++++++++ .../mongodb/core/ReactiveUpdateOperation.java | 48 ++++++- .../core/ReactiveUpdateOperationSupport.java | 64 +++++++-- ...ExecutableUpdateOperationSupportTests.java | 52 ++++++++ .../data/mongodb/core/MongoTemplateTests.java | 56 ++++++++ .../core/ReactiveMongoTemplateTests.java | 84 +++++++++++- .../ReactiveUpdateOperationSupportTests.java | 46 +++++++ src/main/asciidoc/new-features.adoc | 1 + 14 files changed, 970 insertions(+), 25 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java 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 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