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 b93bd11a2..0c0d44fcb 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 @@ -24,8 +24,8 @@ import org.springframework.lang.Nullable; import com.mongodb.client.result.UpdateResult; /** - * {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify / findAndReplace operations in a - * fluent API style.
+ * {@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 * default derived from the initial {@literal domainType} and can be defined there via @@ -59,6 +59,10 @@ public interface ExecutableUpdateOperation { /** * Trigger findAndModify execution by calling one of the terminating methods. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.0 */ interface TerminatingFindAndModify { @@ -81,7 +85,12 @@ public interface ExecutableUpdateOperation { } /** - * Trigger findAndReplace execution by calling one of the terminating methods. + * Trigger + * findOneAndReplace + * execution by calling one of the terminating methods. + * + * @author Mark Paluch + * @since 2.1 */ interface TerminatingFindAndReplace { @@ -158,7 +167,7 @@ public interface ExecutableUpdateOperation { * @throws IllegalArgumentException if options is {@literal null}. * @since 2.1 */ - FindAndReplaceWithOptions replaceWith(T replacement); + FindAndReplaceWithProjection replaceWith(T replacement); } /** @@ -220,6 +229,7 @@ public interface ExecutableUpdateOperation { * Define {@link FindAndReplaceOptions}. * * @author Mark Paluch + * @author Christoph Strobl * @since 2.1 */ interface FindAndReplaceWithOptions extends TerminatingFindAndReplace { @@ -231,7 +241,28 @@ public interface ExecutableUpdateOperation { * @return new instance of {@link FindAndReplaceOptions}. * @throws IllegalArgumentException if options is {@literal null}. */ - TerminatingFindAndReplace withOptions(FindAndReplaceOptions options); + FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options); + } + + /** + * Result type override (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface FindAndReplaceWithProjection extends FindAndReplaceWithOptions { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param resultType must not be {@literal null}. + * @param result type. + * @return new instance of {@link FindAndReplaceWithProjection}. + * @throws IllegalArgumentException if resultType is {@literal null}. + */ + FindAndReplaceWithOptions as(Class resultType); + } /** 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 c56579182..d5de68354 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, null, null); + return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType); } /** @@ -60,17 +60,19 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { */ @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) - static class ExecutableUpdateSupport implements ExecutableUpdate, UpdateWithCollection, UpdateWithQuery, - TerminatingUpdate, FindAndReplaceWithOptions, TerminatingFindAndReplace { + static class ExecutableUpdateSupport + implements ExecutableUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate, + FindAndReplaceWithOptions, TerminatingFindAndReplace, FindAndReplaceWithProjection { @NonNull MongoTemplate template; - @NonNull Class domainType; + @NonNull Class domainType; Query query; @Nullable Update update; @Nullable String collection; @Nullable FindAndModifyOptions findAndModifyOptions; @Nullable FindAndReplaceOptions findAndReplaceOptions; - @Nullable T replacement; + @Nullable Object replacement; + @NonNull Class targetType; /* * (non-Javadoc) @@ -82,7 +84,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(update, "Update must not be null!"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); } /* @@ -95,7 +97,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.hasText(collection, "Collection must not be null nor empty!"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); } /* @@ -108,7 +110,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(options, "Options must not be null!"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); } /* @@ -116,12 +118,12 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { * @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithUpdate#replaceWith(Object) */ @Override - public FindAndReplaceWithOptions replaceWith(T replacement) { + public FindAndReplaceWithProjection replaceWith(T replacement) { Assert.notNull(replacement, "Replacement must not be null!"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); } /* @@ -129,12 +131,12 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { * @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.FindAndReplaceWithOptions#withOptions(org.springframework.data.mongodb.core.FindAndReplaceOptions) */ @Override - public TerminatingFindAndReplace withOptions(FindAndReplaceOptions options) { + public FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options) { Assert.notNull(options, "Options must not be null!"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - options, replacement); + options, replacement, targetType); } /* @@ -147,7 +149,20 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(query, "Query must not be null!"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.FindAndReplaceWithProjection#as(java.lang.Class) + */ + @Override + public FindAndReplaceWithOptions as(Class resultType) { + + Assert.notNull(resultType, "ResultType must not be null!"); + + return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, + findAndReplaceOptions, replacement, resultType); } /* @@ -183,8 +198,9 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { */ @Override public @Nullable T findAndModifyValue() { + return template.findAndModify(query, update, - findAndModifyOptions != null ? findAndModifyOptions : new FindAndModifyOptions(), domainType, + findAndModifyOptions != null ? findAndModifyOptions : new FindAndModifyOptions(), targetType, getCollectionName()); } @@ -194,9 +210,10 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { */ @Override public @Nullable T findAndReplaceValue() { - return template.findAndReplace(query, replacement, - findAndReplaceOptions != null ? findAndReplaceOptions : new FindAndReplaceOptions(), domainType, - getCollectionName()); + + return (T) template.findAndReplace(query, replacement, + findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), domainType, + getCollectionName(), targetType); } private UpdateResult doUpdate(boolean multi, boolean upsert) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java index ce5e13e5d..3d4d3ce7f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java @@ -36,15 +36,17 @@ public class FindAndModifyOptions { /** * Static factory method to create a FindAndModifyOptions instance * - * @return a new instance + * @return new instance of {@link FindAndModifyOptions}. */ public static FindAndModifyOptions options() { return new FindAndModifyOptions(); } /** - * @param options - * @return + * Create new {@link FindAndModifyOptions} based on option of given {@litearl source}. + * + * @param source can be {@literal null}. + * @return new instance of {@link FindAndModifyOptions}. * @since 2.0 */ public static FindAndModifyOptions of(@Nullable FindAndModifyOptions source) { 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 index bea09f74b..c4c6f5263 100644 --- 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 @@ -15,16 +15,20 @@ */ 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. + *
+ * Defaults to + *
+ *
returnNew
+ *
false
+ *
upsert
+ *
false
+ *
* * @author Mark Paluch + * @author Christoph Strobl * @since 2.1 */ public class FindAndReplaceOptions { @@ -32,73 +36,74 @@ public class FindAndReplaceOptions { private boolean returnNew; private boolean upsert; - private @Nullable Collation collation; - /** * Static factory method to create a {@link FindAndReplaceOptions} instance. + *
+ *
returnNew
+ *
false
+ *
upsert
+ *
false
+ *
* - * @return a new instance + * @return new instance of {@link FindAndReplaceOptions}. */ public static FindAndReplaceOptions options() { return new FindAndReplaceOptions(); } /** - * @param options - * @return + * Static factory method to create a {@link FindAndReplaceOptions} instance with + *
+ *
returnNew
+ *
false
+ *
upsert
+ *
false
+ *
+ * + * @return new instance of {@link FindAndReplaceOptions}. */ - 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 static FindAndReplaceOptions empty() { + return new FindAndReplaceOptions(); } - public FindAndReplaceOptions returnNew(boolean returnNew) { - this.returnNew = returnNew; - return this; - } + /** + * Return the replacement document. + * + * @return this. + */ + public FindAndReplaceOptions returnNew() { - public FindAndReplaceOptions upsert(boolean upsert) { - this.upsert = upsert; + this.returnNew = true; return this; } /** - * Define the {@link Collation} specifying language-specific rules for string comparison. + * Insert a new document if not exists. * - * @param collation - * @return + * @return this. */ - public FindAndReplaceOptions collation(@Nullable Collation collation) { + public FindAndReplaceOptions upsert() { - this.collation = collation; + this.upsert = true; return this; } + /** + * Get the bit indicating to return the replacement document. + * + * @return + */ public boolean isReturnNew() { return returnNew; } - public boolean isUpsert() { - return upsert; - } - /** - * Get the {@link Collation} specifying language-specific rules for string comparison. + * Get the bit indicating if to create a new document if not exists. * * @return */ - public Optional getCollation() { - return Optional.ofNullable(collation); + public boolean isUpsert() { + return upsert; } } 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 c06a2998e..ece8ab3d0 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 @@ -28,6 +28,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.mapreduce.GroupBy; @@ -42,6 +43,7 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.util.CloseableIterator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import com.mongodb.ClientSessionOptions; import com.mongodb.Cursor; @@ -381,7 +383,7 @@ public interface MongoOperations extends FluentMongoOperations { * Returns a new {@link BulkOperations} for the given entity type and collection name. * * @param mode the {@link BulkMode} to use for bulk operations, must not be {@literal null}. - * @param entityClass the name of the entity class. Can be {@literal null}. + * @param entityType the name of the entity class. Can be {@literal null}. * @param collectionName the name of the collection to work on, must not be {@literal null} or empty. * @return {@link BulkOperations} on the named collection associated with the given entity class. */ @@ -420,8 +422,6 @@ public interface MongoOperations extends FluentMongoOperations { * Execute a group operation over the entire collection. The group operation entity class should match the 'shape' of * the returned object that takes int account the initial document structure as well as any finalize functions. * - * @param criteria The criteria that restricts the row that are considered for grouping. If not specified all rows are - * considered. * @param inputCollectionName the collection where the group operation will read from * @param groupBy the conditions under which the group operation will be performed, e.g. keys, initial document, * reduce function. @@ -898,7 +898,10 @@ public interface MongoOperations extends FluentMongoOperations { * Triggers *
findOneAndReplace * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} - * document. + * document.
+ * The collection name is derived from the {@literal replacement} type.
+ * Options are defaulted to {@link FindAndReplaceOptions#empty()}.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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}. @@ -908,14 +911,16 @@ public interface MongoOperations extends FluentMongoOperations { */ @Nullable default T findAndReplace(Query query, T replacement) { - return findAndReplace(query, replacement, FindAndReplaceOptions.options()); + return findAndReplace(query, replacement, FindAndReplaceOptions.empty()); } /** * Triggers *
findOneAndReplace * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} - * document. + * document.
+ * Options are defaulted to {@link FindAndReplaceOptions#empty()}.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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}. @@ -926,14 +931,15 @@ public interface MongoOperations extends FluentMongoOperations { */ @Nullable default T findAndReplace(Query query, T replacement, String collectionName) { - return findAndReplace(query, replacement, FindAndReplaceOptions.options(), collectionName); + return findAndReplace(query, replacement, FindAndReplaceOptions.empty(), 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. + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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}. @@ -945,13 +951,16 @@ public interface MongoOperations extends FluentMongoOperations { * @since 2.1 */ @Nullable - T findAndReplace(Query query, T replacement, FindAndReplaceOptions options); + default T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) { + return findAndReplace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement))); + } /** * Triggers *
findOneAndReplace * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document - * taking {@link FindAndReplaceOptions} into account. + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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}. @@ -963,19 +972,24 @@ public interface MongoOperations extends FluentMongoOperations { * @since 2.1 */ @Nullable - T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName); + default T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) { + + Assert.notNull(replacement, "Replacement must not be null!"); + return findAndReplace(query, replacement, options, (Class) ClassUtils.getUserClass(replacement), 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. + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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 entityType 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 @@ -983,8 +997,63 @@ public interface MongoOperations extends FluentMongoOperations { * @since 2.1 */ @Nullable - T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityClass, - String collectionName); + default T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityType, + String collectionName) { + + return findAndReplace(query, replacement, options, entityType, collectionName, entityType); + } + + /** + * Triggers + *
findOneAndReplace + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @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 entityType the type used for mapping the {@link Query} to domain type fields and deriving the collection + * from. Must not be {@literal null}. + * @param resultType the parametrized type projection return type. Must not be {@literal null}, use the domain type of + * {@code Object.class} instead. + * @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 + default T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, + Class resultType) { + + return findAndReplace(query, replacement, options, entityType, + getCollectionName(ClassUtils.getUserClass(entityType)), resultType); + } + + /** + * Triggers + *
findOneAndReplace + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @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 entityType the type used for mapping the {@link Query} to domain type fields. Must not be {@literal null}. + * @param collectionName the collection to query. Must not be {@literal null}. + * @param resultType the parametrized type projection return type. Must not be {@literal null}, use the domain type of + * {@code Object.class} instead. + * @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, S replacement, FindAndReplaceOptions options, Class entityType, + String collectionName, Class resultType); /** * Map the results of an ad-hoc query on the collection for the entity type to a single instance of an object of 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 bec5682b3..f0ac8e53a 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 @@ -1072,60 +1072,33 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, /* * (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) + * @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, java.lang.Class) */ @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) { + public T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, + String collectionName, Class resultType) { 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(options, "Options must not be null! Use FindAndReplaceOptions#empty() instead."); + Assert.notNull(entityType, "EntityType must not be null!"); Assert.notNull(collectionName, "CollectionName must not be null!"); + Assert.notNull(resultType, "ResultType must not be null! Use Object.class instead."); - FindAndReplaceOptions optionsToUse = FindAndReplaceOptions.of(options); + Assert.isTrue(query.getLimit() <= 1, "Query must not define a limit other than 1 ore none!"); + Assert.isTrue(query.getSkip() <= 0, "Query must not define skip."); - 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."); - }); + MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityType); - query.getCollation().ifPresent(optionsToUse::collation); + Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), entity); + Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), entity); + Document mappedSort = queryMapper.getMappedSort(query.getSortObject(), entity); - return doFindAndReplace(collectionName, query.getQueryObject(), query.getFieldsObject(), query.getSortObject(), - entityClass, replacement, options); + Document mappedReplacement = toDocument(replacement, this.mongoConverter); + + return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, + query.getCollation().map(Collation::toMongoCollation).orElse(null), entityType, mappedReplacement, options, + resultType); } // Find methods that take a Query to express the query and that return a single object that is also removed from the @@ -2661,30 +2634,38 @@ 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); + /** + * Customize this part for findAndReplace. + * + * @param collectionName The name of the collection to perform the operation in. + * @param mappedQuery the query to look up documents. + * @param mappedFields the fields to project the result to. + * @param mappedSort the sort to be applied when executing the query. + * @param collation collation settings for the query. Can be {@literal null}. + * @param entityType the source domain type. + * @param replacement the replacement {@link Document}. + * @param options applicable options. + * @param resultType the target domain type. + * @return {@literal null} if object does not exist, {@link FindAndReplaceOptions#isReturnNew() return new} is + * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. + */ + @Nullable + protected T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, + Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class entityType, + Document replacement, FindAndReplaceOptions options, Class resultType) { if (LOGGER.isDebugEnabled()) { LOGGER.debug( "findAndReplace using query: {} fields: {} sort: {} for class: {} and replacement: {} " + "in collection: {}", - serializeToJsonSafely(mappedQuery), fields, sort, entityClass, serializeToJsonSafely(dbDoc), collectionName); + serializeToJsonSafely(mappedQuery), serializeToJsonSafely(mappedFields), serializeToJsonSafely(mappedSort), + entityType, serializeToJsonSafely(replacement), collectionName); } - maybeEmitEvent(new BeforeSaveEvent<>(replacement, dbDoc, collectionName)); + maybeEmitEvent(new BeforeSaveEvent<>(replacement, replacement, collectionName)); - return executeFindOneInternal(new FindAndReplaceCallback(mappedQuery, fields, sort, dbDoc, options), - new ReadDocumentCallback<>(readerToUse, entityClass, collectionName), collectionName); + return executeFindOneInternal( + new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), + new ProjectingReadCallback<>(mongoConverter, entityType, resultType, collectionName), collectionName); } /** @@ -2747,6 +2728,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @param collectionName the collection to be queried * @return */ + @Nullable private T executeFindOneInternal(CollectionCallback collectionCallback, DocumentCallback objectCallback, String collectionName) { @@ -3102,37 +3084,53 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } } + /** + * {@link CollectionCallback} specific for find and remove operation. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 + */ private static class FindAndReplaceCallback implements CollectionCallback { private final Document query; private final Document fields; private final Document sort; private final Document update; + private final @Nullable com.mongodb.client.model.Collation collation; private final FindAndReplaceOptions options; - public FindAndReplaceCallback(Document query, Document fields, Document sort, Document update, - FindAndReplaceOptions options) { + FindAndReplaceCallback(Document query, Document fields, Document sort, Document update, + com.mongodb.client.model.Collation collation, FindAndReplaceOptions options) { + this.query = query; this.fields = fields; this.sort = sort; this.update = update; this.options = options; + this.collation = collation; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.CollectionCallback#doInCollection(com.mongodb.client.MongoCollection) + */ + @Override public Document doInCollection(MongoCollection collection) throws MongoException, DataAccessException { FindOneAndReplaceOptions opts = new FindOneAndReplaceOptions(); opts.sort(sort); + opts.collation(collation); + opts.projection(fields); + 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); } } 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 de5844d5b..63c1c21ea 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 @@ -41,6 +41,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import com.mongodb.ClientSessionOptions; import com.mongodb.ReadPreference; @@ -692,7 +693,9 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * Triggers *
findOneAndReplace * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} - * document. + * document.
+ * Options are defaulted to {@link FindAndReplaceOptions#empty()}.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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}. @@ -701,14 +704,16 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @since 2.1 */ default Mono findAndReplace(Query query, T replacement) { - return findAndReplace(query, replacement, FindAndReplaceOptions.options()); + return findAndReplace(query, replacement, FindAndReplaceOptions.empty()); } /** * Triggers *
findOneAndReplace * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} - * document. + * document.
+ * Options are defaulted to {@link FindAndReplaceOptions#empty()}.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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}. @@ -718,14 +723,15 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @since 2.1 */ default Mono findAndReplace(Query query, T replacement, String collectionName) { - return findAndReplace(query, replacement, FindAndReplaceOptions.options(), collectionName); + return findAndReplace(query, replacement, FindAndReplaceOptions.empty(), 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. + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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}. @@ -736,13 +742,16 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * as it is after the update. * @since 2.1 */ - Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options); + default Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options) { + return findAndReplace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement))); + } /** * Triggers *
findOneAndReplace * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document - * taking {@link FindAndReplaceOptions} into account. + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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}. @@ -753,27 +762,86 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * as it is after the update. * @since 2.1 */ - Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName); + default Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) { + + Assert.notNull(replacement, "Replacement must not be null!"); + return findAndReplace(query, replacement, options, (Class) ClassUtils.getUserClass(replacement), 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. + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. * * @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 entityType 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); + default Mono findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityType, + String collectionName) { + + return findAndReplace(query, replacement, options, entityType, collectionName, entityType); + } + + /** + * Triggers + *
findOneAndReplace + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @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 entityType the type used for mapping the {@link Query} to domain type fields and deriving the collection + * from. Must not be {@literal null}. + * @param resultType the parametrized type projection return type. Must not be {@literal null}, use the domain type of + * {@code Object.class} instead. + * @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 + */ + default Mono findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, + Class resultType) { + + return findAndReplace(query, replacement, options, entityType, + getCollectionName(ClassUtils.getUserClass(entityType)), resultType); + } + + /** + * Triggers + *
findOneAndReplace + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document + * taking {@link FindAndReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @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 entityType the type used for mapping the {@link Query} to domain type fields and deriving the collection + * from. Must not be {@literal null}. + * @param collectionName the collection to query. Must not be {@literal null}. + * @param resultType resultType the parametrized type projection return type. Must not be {@literal null}, use the + * domain type of {@code Object.class} instead. + * @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, S replacement, FindAndReplaceOptions options, Class entityType, + String collectionName, Class resultType); /** * Map the results of an ad-hoc query on the collection for the entity type to a single instance of an object of the @@ -1375,4 +1443,13 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { */ MongoConverter getConverter(); + /** + * The collection name used for the specified class by this template. + * + * @param entityClass must not be {@literal null}. + * @return + * @since 2.1 + */ + String getCollectionName(Class entityClass); + } 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 14c0ec883..2a5ba2a8a 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 @@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.SerializationUtils.*; +import lombok.AccessLevel; import lombok.NonNull; import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; @@ -114,13 +115,30 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; -import com.mongodb.*; +import com.mongodb.BasicDBObject; +import com.mongodb.ClientSessionOptions; +import com.mongodb.CursorType; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBRef; +import com.mongodb.Mongo; +import com.mongodb.MongoException; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; import com.mongodb.client.model.*; import com.mongodb.client.model.changestream.FullDocument; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; -import com.mongodb.reactivestreams.client.*; +import com.mongodb.reactivestreams.client.AggregatePublisher; +import com.mongodb.reactivestreams.client.ChangeStreamPublisher; +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.DistinctPublisher; +import com.mongodb.reactivestreams.client.FindPublisher; +import com.mongodb.reactivestreams.client.MapReducePublisher; import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import com.mongodb.reactivestreams.client.Success; import com.mongodb.util.JSONParseException; /** @@ -1084,59 +1102,33 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati /* * (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) + * @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, java.lang.Class) */ @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) { + public Mono findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, + String collectionName, Class resultType) { 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(options, "Options must not be null! Use FindAndReplaceOptions#empty() instead."); + Assert.notNull(entityType, "Entity class must not be null!"); Assert.notNull(collectionName, "CollectionName must not be null!"); + Assert.notNull(resultType, "ResultType must not be null! Use Object.class instead."); - FindAndReplaceOptions optionsToUse = FindAndReplaceOptions.of(options); + Assert.isTrue(query.getLimit() <= 1, "Query must not define a limit other than 1 ore none!"); + Assert.isTrue(query.getSkip() <= 0, "Query must not define skip."); - 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."); - }); + MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityType); - query.getCollation().ifPresent(optionsToUse::collation); + Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), entity); + Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), entity); + Document mappedSort = queryMapper.getMappedSort(query.getSortObject(), entity); + + Document mappedReplacement = toDocument(replacement, this.mongoConverter); - return doFindAndReplace(collectionName, query.getQueryObject(), query.getFieldsObject(), query.getSortObject(), - entityClass, replacement, options); + return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, + query.getCollation().map(Collation::toMongoCollation).orElse(null), entityType, mappedReplacement, options, + resultType); } /* @@ -2481,28 +2473,41 @@ 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); + /** + * Customize this part for findAndReplace. + * + * @param collectionName The name of the collection to perform the operation in. + * @param mappedQuery the query to look up documents. + * @param mappedFields the fields to project the result to. + * @param mappedSort the sort to be applied when executing the query. + * @param collation collation settings for the query. Can be {@literal null}. + * @param entityType the source domain type. + * @param replacement the replacement {@link Document}. + * @param options applicable options. + * @param resultType the target domain type. + * @return {@link Mono#empty()} if object does not exist, {@link FindAndReplaceOptions#isReturnNew() return new} is + * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. + * @since 2.1 + */ + protected Mono doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, + Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, Class resultType) { return Mono.defer(() -> { - 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); + serializeToJsonSafely(mappedQuery), mappedFields, mappedSort, entityType, + serializeToJsonSafely(replacement), collectionName); } - maybeEmitEvent(new BeforeSaveEvent<>(replacement, dbDoc, collectionName)); + maybeEmitEvent(new BeforeSaveEvent<>(replacement, replacement, collectionName)); - return executeFindOneInternal(new FindAndReplaceCallback(mappedQuery, fields, sort, dbDoc, options), - new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); + return executeFindOneInternal( + new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), + new ProjectingReadCallback<>(this.mongoConverter, entityType, resultType, collectionName), collectionName); }); } @@ -2988,17 +2993,26 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } /** + * {@link ReactiveCollectionCallback} specific for find and remove operation. + * * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 */ - @RequiredArgsConstructor + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) private static class FindAndReplaceCallback implements ReactiveCollectionCallback { private final Document query; private final Document fields; private final Document sort; private final Document update; + private final @Nullable com.mongodb.client.model.Collation collation; private final FindAndReplaceOptions options; + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveCollectionCallback#doInCollection(com.mongodb.reactivestreams.client.MongoCollection) + */ @Override public Publisher doInCollection(MongoCollection collection) throws MongoException, DataAccessException { @@ -3010,7 +3024,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private FindOneAndReplaceOptions convertToFindOneAndReplaceOptions(FindAndReplaceOptions options, Document fields, Document sort) { - FindOneAndReplaceOptions result = new FindOneAndReplaceOptions(); + FindOneAndReplaceOptions result = new FindOneAndReplaceOptions().collation(collation); result = result.projection(fields).sort(sort).upsert(options.isUpsert()); @@ -3020,8 +3034,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati result = result.returnDocument(ReturnDocument.BEFORE); } - result = options.getCollation().map(Collation::toMongoCollation).map(result::collation).orElse(result); - return result; } } 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 002beb366..75ed0af74 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 @@ -72,6 +72,7 @@ public interface ReactiveUpdateOperation { /** * Compose findAndReplace execution by calling one of the terminating methods. * + * @author Mark Paluch * @since 2.1 */ interface TerminatingFindAndReplace { @@ -133,7 +134,7 @@ public interface ReactiveUpdateOperation { * @throws IllegalArgumentException if options is {@literal null}. * @since 2.1 */ - FindAndReplaceWithOptions replaceWith(T replacement); + FindAndReplaceWithProjection replaceWith(T replacement); } /** @@ -187,6 +188,7 @@ public interface ReactiveUpdateOperation { * Define {@link FindAndReplaceOptions}. * * @author Mark Paluch + * @author Christoph Strobl * @since 2.1 */ interface FindAndReplaceWithOptions extends TerminatingFindAndReplace { @@ -198,7 +200,28 @@ public interface ReactiveUpdateOperation { * @return new instance of {@link FindAndReplaceOptions}. * @throws IllegalArgumentException if options is {@literal null}. */ - TerminatingFindAndReplace withOptions(FindAndReplaceOptions options); + FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options); + } + + /** + * Result type override (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface FindAndReplaceWithProjection extends FindAndReplaceWithOptions { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param resultType must not be {@literal null}. + * @param result type. + * @return new instance of {@link FindAndReplaceWithProjection}. + * @throws IllegalArgumentException if resultType is {@literal null}. + */ + FindAndReplaceWithOptions as(Class resultType); + } 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 11feea58a..82ebcdce7 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 @@ -51,22 +51,24 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { Assert.notNull(domainType, "DomainType must not be null!"); - return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null); + return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType); } @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) - static class ReactiveUpdateSupport implements ReactiveUpdate, UpdateWithCollection, UpdateWithQuery, - TerminatingUpdate, FindAndReplaceWithOptions, TerminatingFindAndReplace { + static class ReactiveUpdateSupport + implements ReactiveUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate, + FindAndReplaceWithOptions, FindAndReplaceWithProjection, TerminatingFindAndReplace { @NonNull ReactiveMongoTemplate template; - @NonNull Class domainType; + @NonNull Class domainType; Query query; org.springframework.data.mongodb.core.query.Update update; @Nullable String collection; @Nullable FindAndModifyOptions findAndModifyOptions; @Nullable FindAndReplaceOptions findAndReplaceOptions; - @Nullable T replacement; + @Nullable Object replacement; + @NonNull Class targetType; /* * (non-Javadoc) @@ -78,7 +80,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { Assert.notNull(update, "Update must not be null!"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); } /* @@ -91,7 +93,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { Assert.hasText(collection, "Collection must not be null nor empty!"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); } /* @@ -121,7 +123,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { String collectionName = getCollectionName(); - return template.findAndModify(query, update, findAndModifyOptions, domainType, collectionName); + return template.findAndModify(query, update, findAndModifyOptions, targetType, collectionName); } /* @@ -131,8 +133,8 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { @Override public Mono findAndReplace() { return template.findAndReplace(query, replacement, - findAndReplaceOptions != null ? findAndReplaceOptions : new FindAndReplaceOptions(), domainType, - getCollectionName()); + findAndReplaceOptions != null ? findAndReplaceOptions : new FindAndReplaceOptions(), (Class) domainType, + getCollectionName(), targetType); } /* @@ -145,7 +147,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { Assert.notNull(query, "Query must not be null!"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); } /* @@ -167,7 +169,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { Assert.notNull(options, "Options must not be null!"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); } /* @@ -175,12 +177,12 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { * @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithUpdate#replaceWith(java.lang.Object) */ @Override - public FindAndReplaceWithOptions replaceWith(T replacement) { + public FindAndReplaceWithProjection replaceWith(T replacement) { Assert.notNull(replacement, "Replacement must not be null!"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement); + findAndReplaceOptions, replacement, targetType); } /* @@ -188,12 +190,25 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { * @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.FindAndReplaceWithOptions#withOptions(org.springframework.data.mongodb.core.FindAndReplaceOptions) */ @Override - public TerminatingFindAndReplace withOptions(FindAndReplaceOptions options) { + public FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options) { Assert.notNull(options, "Options must not be null!"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, options, - replacement); + replacement, targetType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.FindAndReplaceWithProjection#as(java.lang.Class) + */ + @Override + public FindAndReplaceWithOptions as(Class resultType) { + + Assert.notNull(resultType, "ResultType must not be null!"); + + return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, + findAndReplaceOptions, replacement, resultType); } 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 fd8b6adac..e8d5774f6 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 @@ -226,11 +226,23 @@ public class ExecutableUpdateOperationSupportTests { luke.firstname = "Luke"; Person result = template.update(Person.class).matching(queryHan()).replaceWith(luke) - .withOptions(FindAndReplaceOptions.options().returnNew(true)).findAndReplaceValue(); + .withOptions(FindAndReplaceOptions.options().returnNew()).findAndReplaceValue(); assertThat(result).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Luke"); } + @Test // DATAMONGO-1827 + public void findAndReplaceWithProjection() { + + Person luke = new Person(); + luke.firstname = "Luke"; + + Jedi result = template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class) + .findAndReplaceValue(); + + assertThat(result.getName()).isEqualTo(han.firstname); + } + 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 368d194f5..6916d1f3d 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 @@ -26,6 +26,7 @@ import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; import static org.springframework.data.mongodb.core.query.Update.*; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -52,6 +53,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; @@ -78,7 +80,6 @@ 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; @@ -2390,6 +2391,51 @@ public class MongoTemplateTests { assertThat(template.findOne(query(where("foo").is("baz")), org.bson.Document.class, "findandreplace")).isNotNull(); } + @Test // DATAMONGO-1827 + public void findAndReplaceShouldErrorOnIdPresent() { + + thrown.expect(InvalidDataAccessApiUsageException.class); + + template.save(new MyPerson("Walter")); + + MyPerson replacement = new MyPerson("Heisenberg"); + replacement.id = "invalid-id"; + + template.findAndReplace(query(where("name").is("Walter")), replacement); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceShouldErrorOnSkip() { + + thrown.expect(IllegalArgumentException.class); + + template.findAndReplace(query(where("name").is("Walter")).skip(10), new MyPerson("Heisenberg")); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceShouldErrorOnLimit() { + + thrown.expect(IllegalArgumentException.class); + + template.findAndReplace(query(where("name").is("Walter")).limit(10), new MyPerson("Heisenberg")); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceShouldConsiderSortAndUpdateFirstIfMultipleFound() { + + MyPerson walter1 = new MyPerson("Walter 1"); + MyPerson walter2 = new MyPerson("Walter 2"); + + template.save(walter1); + template.save(walter2); + + MyPerson replacement = new MyPerson("Heisenberg"); + + template.findAndReplace(query(where("name").regex("Walter.*")).with(Sort.by(Direction.DESC, "name")), replacement); + + assertThat(template.findAll(MyPerson.class)).hasSize(2).contains(walter1).doesNotContain(walter2); + } + @Test // DATAMONGO-1827 public void findAndReplaceShouldReplaceObject() { @@ -2399,7 +2445,43 @@ public class MongoTemplateTests { 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(); + assertThat(template.findOne(query(where("id").is(person.id)), MyPerson.class)).hasFieldOrPropertyWithValue("name", + "Heisenberg"); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceShouldConsiderFields() { + + MyPerson person = new MyPerson("Walter"); + person.address = new Address("TX", "Austin"); + template.save(person); + + Query query = query(where("name").is("Walter")); + query.fields().include("address"); + + MyPerson previous = template.findAndReplace(query, new MyPerson("Heisenberg")); + + assertThat(previous.getName()).isNull(); + assertThat(previous.getAddress()).isEqualTo(person.address); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceNonExistingWithUpsertFalse() { + + MyPerson previous = template.findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg")); + + assertThat(previous).isNull(); + assertThat(template.findAll(MyPerson.class)).isEmpty(); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceNonExistingWithUpsertTrue() { + + MyPerson previous = template.findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg"), + FindAndReplaceOptions.options().upsert()); + + assertThat(previous).isNull(); + assertThat(template.findAll(MyPerson.class)).hasSize(1); } @Test // DATAMONGO-1827 @@ -2409,19 +2491,20 @@ public class MongoTemplateTests { template.save(person); MyPerson updated = template.findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg"), - FindAndReplaceOptions.options().returnNew(true)); + FindAndReplaceOptions.options().returnNew()); assertThat(updated.getName()).isEqualTo("Heisenberg"); } @Test // DATAMONGO-1827 - public void findAndReplaceShouldFailWithTwoCollationObjects() { + public void findAndReplaceShouldProjectReturnedObjectCorrectly() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Both Query and FindAndReplaceOptions"); + template.save(new MyPerson("Walter")); + + MyPersonProjection projection = template.findAndReplace(query(where("name").is("Walter")), + new MyPerson("Heisenberg"), FindAndReplaceOptions.empty(), MyPerson.class, MyPersonProjection.class); - template.findAndReplace(query(where("name").is("Walter")).collation(Collation.of("de")), new MyPerson("Heisenberg"), - FindAndReplaceOptions.options().collation(Collation.of("en"))); + assertThat(projection.getName()).isEqualTo("Walter"); } @Test // DATAMONGO-407 @@ -3422,7 +3505,8 @@ public class MongoTemplateTests { template.save(source); - DocumentWithNestedTypeHavingStringIdProperty target = template.query(DocumentWithNestedTypeHavingStringIdProperty.class) + DocumentWithNestedTypeHavingStringIdProperty target = template + .query(DocumentWithNestedTypeHavingStringIdProperty.class) .matching(query(where("sample.id").is(source.sample.id))).firstValue(); assertThat(target).isEqualTo(source); @@ -3652,21 +3736,23 @@ public class MongoTemplateTests { } } + @Data + @NoArgsConstructor + @AllArgsConstructor public static class MyPerson { String id; String name; Address address; - public MyPerson() {} - public MyPerson(String name) { this.name = name; } + } - public String getName() { - return name; - } + interface MyPersonProjection { + + String getName(); } static class Address { 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 075c4cb15..95b224b8d 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 @@ -20,7 +20,9 @@ import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; import static org.springframework.data.mongodb.test.util.Assertions.*; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -51,6 +53,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Sort; @@ -64,7 +67,6 @@ 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; @@ -476,6 +478,52 @@ public class ReactiveMongoTemplateTests { .verifyComplete(); } + @Test // DATAMONGO-1827 + public void findAndReplaceShouldErrorOnIdPresent() { + + template.save(new MyPerson("Walter")).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + + MyPerson replacement = new MyPerson("Heisenberg"); + replacement.id = "invalid-id"; + + template.findAndReplace(query(where("name").is("Walter")), replacement) // + .as(StepVerifier::create) // + .expectError(InvalidDataAccessApiUsageException.class); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceShouldErrorOnSkip() { + + thrown.expect(IllegalArgumentException.class); + + template.findAndReplace(query(where("name").is("Walter")).skip(10), new MyPerson("Heisenberg")).subscribe(); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceShouldErrorOnLimit() { + + thrown.expect(IllegalArgumentException.class); + + template.findAndReplace(query(where("name").is("Walter")).limit(10), new MyPerson("Heisenberg")).subscribe(); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceShouldConsiderSortAndUpdateFirstIfMultipleFound() { + + MyPerson walter1 = new MyPerson("Walter 1"); + MyPerson walter2 = new MyPerson("Walter 2"); + + template.save(walter1).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + template.save(walter2).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + MyPerson replacement = new MyPerson("Heisenberg"); + + template.findAndReplace(query(where("name").regex("Walter.*")).with(Sort.by(Direction.DESC, "name")), replacement) + .as(StepVerifier::create).expectNextCount(1).verifyComplete(); + + template.findAll(MyPerson.class).buffer(10).as(StepVerifier::create) + .consumeNextWith(it -> assertThat(it).hasSize(2).contains(walter1).doesNotContain(walter2)).verifyComplete(); + } + @Test // DATAMONGO-1827 public void findAndReplaceShouldReplaceObject() { @@ -493,28 +541,74 @@ public class ReactiveMongoTemplateTests { } @Test // DATAMONGO-1827 - public void findAndReplaceShouldReplaceObjectReturingNew() { + public void findAndReplaceShouldConsiderFields() { MyPerson person = new MyPerson("Walter"); + person.address = new Address("TX", "Austin"); template.save(person).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + Query query = query(where("name").is("Walter")); + query.fields().include("address"); + + template.findAndReplace(query, new MyPerson("Heisenberg")) // + .as(StepVerifier::create) // + .consumeNextWith(it -> { + + assertThat(it.getName()).isNull(); + assertThat(it.getAddress()).isEqualTo(person.address); + }).verifyComplete(); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceNonExistingWithUpsertFalse() { + + template.findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg")) // + .as(StepVerifier::create) // + .verifyComplete(); + + StepVerifier.create(template.findAll(MyPerson.class)).verifyComplete(); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceNonExistingWithUpsertTrue() { + template .findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg"), - FindAndReplaceOptions.options().returnNew(true)) + FindAndReplaceOptions.options().upsert()) // + .as(StepVerifier::create) // + .verifyComplete(); + + StepVerifier.create(template.findAll(MyPerson.class)).expectNextCount(1).verifyComplete(); + } + + @Test // DATAMONGO-1827 + public void findAndReplaceShouldProjectReturnedObjectCorrectly() { + + 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.empty(), + MyPerson.class, MyPersonProjection.class) // .as(StepVerifier::create) // .consumeNextWith(actual -> { - assertThat(actual.getName()).isEqualTo("Heisenberg"); + assertThat(actual.getName()).isEqualTo("Walter"); }).verifyComplete(); } @Test // DATAMONGO-1827 - public void findAndReplaceShouldFailWithTwoCollationObjects() { + public void findAndReplaceShouldReplaceObjectReturingNew() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Both Query and FindAndReplaceOptions"); + MyPerson person = new MyPerson("Walter"); + template.save(person).as(StepVerifier::create).expectNextCount(1).verifyComplete(); - template.findAndReplace(query(where("name").is("Walter")).collation(Collation.of("de")), new MyPerson("Heisenberg"), - FindAndReplaceOptions.options().collation(Collation.of("en"))); + template + .findAndReplace(query(where("name").is("Walter")), new MyPerson("Heisenberg"), + FindAndReplaceOptions.options().returnNew()) + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual.getName()).isEqualTo("Heisenberg"); + }).verifyComplete(); } @Test // DATAMONGO-1444 @@ -1243,20 +1337,21 @@ public class ReactiveMongoTemplateTests { } } + @Data + @NoArgsConstructor + @AllArgsConstructor public static class MyPerson { String id; String name; Address address; - public MyPerson() {} - public MyPerson(String name) { this.name = name; } + } - public String getName() { - return name; - } + interface MyPersonProjection { + String getName(); } } 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 13ae3e971..7e3530b5f 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 @@ -197,6 +197,18 @@ public class ReactiveUpdateOperationSupportTests { }).verifyComplete(); } + @Test // DATAMONGO-1827 + public void findAndReplaceWithProjection() { + + Person luke = new Person(); + luke.firstname = "Luke"; + + template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class).findAndReplace() // + .as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.getName()).isEqualTo(han.firstname); + }).verifyComplete(); + } + @Test // DATAMONGO-1827 public void findAndReplaceWithCollection() { @@ -220,7 +232,7 @@ public class ReactiveUpdateOperationSupportTests { luke.firstname = "Luke"; template.update(Person.class).matching(queryHan()).replaceWith(luke) - .withOptions(FindAndReplaceOptions.options().returnNew(true)).findAndReplace() // + .withOptions(FindAndReplaceOptions.options().returnNew()).findAndReplace() // .as(StepVerifier::create) // .consumeNextWith(actual -> { assertThat(actual).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Luke"); diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 592c18887..0d0c49702 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -14,7 +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. +* <> support through imperative and reactive Template APIs. [[new-features.2-0-0]] == What's New in Spring Data MongoDB 2.0 diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 90bef092d..72816866f 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -437,7 +437,7 @@ NOTE: Once configured, `MongoTemplate` is thread-safe and can be reused across m The mapping between MongoDB documents and domain classes is done by delegating to an implementation of the `MongoConverter` interface. Spring provides `MappingMongoConverter`, but you can also write your own converter. See "`<>`" for more detailed information. -The `MongoTemplate` class implements the interface `MongoOperations`. In as much as possible, the methods on `MongoOperations` are named after methods available on the MongoDB driver `Collection` object, to make the API familiar to existing MongoDB developers who are used to the driver API. For example, you can find methods such as `find`, `findAndModify`, `findOne`, `insert`, `remove`, `save`, `update`, and `updateMulti`. The design goal was to make it as easy as possible to transition between the use of the base MongoDB driver and `MongoOperations`. A major difference between the two APIs is that `MongoOperations` can be passed domain objects instead of `Document`. Also, `MongoOperations` has fluent APIs for `Query`, `Criteria`, and `Update` operations instead of populating a `Document` to specify the parameters for those operations. +The `MongoTemplate` class implements the interface `MongoOperations`. In as much as possible, the methods on `MongoOperations` are named after methods available on the MongoDB driver `Collection` object, to make the API familiar to existing MongoDB developers who are used to the driver API. For example, you can find methods such as `find`, `findAndModify`, `findAndReplace`, `findOne`, `insert`, `remove`, `save`, `update`, and `updateMulti`. The design goal was to make it as easy as possible to transition between the use of the base MongoDB driver and `MongoOperations`. A major difference between the two APIs is that `MongoOperations` can be passed domain objects instead of `Document`. Also, `MongoOperations` has fluent APIs for `Query`, `Criteria`, and `Update` operations instead of populating a `Document` to specify the parameters for those operations. NOTE: The preferred way to reference the operations on `MongoTemplate` instance is through its interface, `MongoOperations`. @@ -967,6 +967,35 @@ assertThat(p.getFirstName(), is("Mary")); assertThat(p.getAge(), is(1)); ---- +[[mongo-template.find-and-replace]] +=== Finding and Replacing Documents + +The most straight forward method of replacing an entire `Document` is via its `id` using the `save` method. However this +might not always be feasible. `findAndReplace` offers an alternative that allows to identify the document to replace via +a simple query. + +.Find and Replace Documents +==== +[source,java] +---- +Optional result = template.update(Person.class) <1> + .matching(query(where("firstame").is("Tom"))) <2> + .replaceWith(new Person("Dick")) + .withOptions(FindAndReplaceOptions.options().upsert()) <3> + .as(User.class) <4> + .findAndReplace(); <5> +---- +<1> Use the fluent update API with the domain type given for mapping the query and deriving the collection name or just use `MongoOperations#findAndReplace`. +<2> The actual match query mapped against the given domain type. Provide `sort`, `fields` and `collation` settings via the query. +<3> Additional optional hook to provide options other than the defaults, like `upsert`. +<4> An optional projection type used for mapping the operation result. If none given the initial domain type is used. +<5> Trigger the actual execution. Use `findAndReplaceValue` to obtain the nullable result instead of an `Optional`. +==== + +IMPORTANT: Please note that the replacement must not hold an `id` itself as the `id` of the existing `Document` will be +carried over to the replacement by the store itself. Also keep in mind that `findAndReplace` will only replace the first +document matching the query criteria depending on a potentially given sort order. + [[mongo-template.delete]] === Methods for Removing Documents