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 super T, Bson> readerToUse = this.mongoConverter;
-
- if (options == null) {
- options = new FindAndReplaceOptions();
- }
-
- MongoPersistentEntity> entity = mappingContext.getPersistentEntity(entityClass);
-
- Document mappedQuery = queryMapper.getMappedObject(query, entity);
- Document dbDoc = toDocument(replacement, this.mongoConverter);
+ /**
+ * 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