Browse Source

DATAMONGO-1827 - Polishing.

Allow open/close projection on return type for findAndReplace.
Use default methods for delegation and remove collation from FindAndRemoveOption in favor of the collation set on the query itself.
Update Javadoc and reference documentation.

Original Pull Request: #569
pull/576/merge
Christoph Strobl 8 years ago
parent
commit
bd5815dbcb
  1. 41
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java
  2. 51
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java
  3. 8
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java
  4. 87
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java
  5. 99
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
  6. 130
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  7. 101
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
  8. 134
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  9. 27
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java
  10. 47
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java
  11. 14
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java
  12. 114
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
  13. 123
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java
  14. 14
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java
  15. 2
      src/main/asciidoc/new-features.adoc
  16. 31
      src/main/asciidoc/reference/mongodb.adoc

41
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java

@ -24,8 +24,8 @@ import org.springframework.lang.Nullable; @@ -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. <br />
* {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify / findAndReplace
* operations in a fluent API style. <br />
* The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, as well as
* the {@link Update} via {@code apply} into the MongoDB specific representations. The collection to operate on is by
* default derived from the initial {@literal domainType} and can be defined there via
@ -59,6 +59,10 @@ public interface ExecutableUpdateOperation { @@ -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<T> {
@ -81,7 +85,12 @@ public interface ExecutableUpdateOperation { @@ -81,7 +85,12 @@ public interface ExecutableUpdateOperation {
}
/**
* Trigger findAndReplace execution by calling one of the terminating methods.
* Trigger
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* execution by calling one of the terminating methods.
*
* @author Mark Paluch
* @since 2.1
*/
interface TerminatingFindAndReplace<T> {
@ -158,7 +167,7 @@ public interface ExecutableUpdateOperation { @@ -158,7 +167,7 @@ public interface ExecutableUpdateOperation {
* @throws IllegalArgumentException if options is {@literal null}.
* @since 2.1
*/
FindAndReplaceWithOptions<T> replaceWith(T replacement);
FindAndReplaceWithProjection<T> replaceWith(T replacement);
}
/**
@ -220,6 +229,7 @@ public interface ExecutableUpdateOperation { @@ -220,6 +229,7 @@ public interface ExecutableUpdateOperation {
* Define {@link FindAndReplaceOptions}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T> {
@ -231,7 +241,28 @@ public interface ExecutableUpdateOperation { @@ -231,7 +241,28 @@ public interface ExecutableUpdateOperation {
* @return new instance of {@link FindAndReplaceOptions}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
TerminatingFindAndReplace<T> withOptions(FindAndReplaceOptions options);
FindAndReplaceWithProjection<T> withOptions(FindAndReplaceOptions options);
}
/**
* Result type override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface FindAndReplaceWithProjection<T> extends FindAndReplaceWithOptions<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link FindAndReplaceWithProjection}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/
<R> FindAndReplaceWithOptions<R> as(Class<R> resultType);
}
/**

51
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java

@ -51,7 +51,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { @@ -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 { @@ -60,17 +60,19 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
*/
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
static class ExecutableUpdateSupport<T> implements ExecutableUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>,
TerminatingUpdate<T>, FindAndReplaceWithOptions<T>, TerminatingFindAndReplace<T> {
static class ExecutableUpdateSupport<T>
implements ExecutableUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T>,
FindAndReplaceWithOptions<T>, TerminatingFindAndReplace<T>, FindAndReplaceWithProjection<T> {
@NonNull MongoTemplate template;
@NonNull Class<T> 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<T> targetType;
/*
* (non-Javadoc)
@ -82,7 +84,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { @@ -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 { @@ -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 { @@ -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 { @@ -116,12 +118,12 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
* @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithUpdate#replaceWith(Object)
*/
@Override
public FindAndReplaceWithOptions<T> replaceWith(T replacement) {
public FindAndReplaceWithProjection<T> 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 { @@ -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<T> withOptions(FindAndReplaceOptions options) {
public FindAndReplaceWithProjection<T> 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 { @@ -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 <R> FindAndReplaceWithOptions<R> as(Class<R> 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 { @@ -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 { @@ -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) {

8
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java

@ -36,15 +36,17 @@ public class FindAndModifyOptions { @@ -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) {

87
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java

@ -15,16 +15,20 @@ @@ -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
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>.
* <br />
* Defaults to
* <dl>
* <dt>returnNew</dt>
* <dd>false</dd>
* <dt>upsert</dt>
* <dd>false</dd>
* </dl>
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
public class FindAndReplaceOptions {
@ -32,73 +36,74 @@ 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.
* <dl>
* <dt>returnNew</dt>
* <dd>false</dd>
* <dt>upsert</dt>
* <dd>false</dd>
* </dl>
*
* @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
* <dl>
* <dt>returnNew</dt>
* <dd>false</dd>
* <dt>upsert</dt>
* <dd>false</dd>
* </dl>
*
* @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<Collation> getCollation() {
return Optional.ofNullable(collation);
public boolean isUpsert() {
return upsert;
}
}

99
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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -898,7 +898,10 @@ public interface MongoOperations extends FluentMongoOperations {
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
* document.
* document. <br />
* The collection name is derived from the {@literal replacement} type. <br />
* Options are defaulted to {@link FindAndReplaceOptions#empty()}. <br />
* <strong>NOTE:</strong> 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 { @@ -908,14 +911,16 @@ public interface MongoOperations extends FluentMongoOperations {
*/
@Nullable
default <T> T findAndReplace(Query query, T replacement) {
return findAndReplace(query, replacement, FindAndReplaceOptions.options());
return findAndReplace(query, replacement, FindAndReplaceOptions.empty());
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
* document.
* document.<br />
* Options are defaulted to {@link FindAndReplaceOptions#empty()}. <br />
* <strong>NOTE:</strong> 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 { @@ -926,14 +931,15 @@ public interface MongoOperations extends FluentMongoOperations {
*/
@Nullable
default <T> T findAndReplace(Query query, T replacement, String collectionName) {
return findAndReplace(query, replacement, FindAndReplaceOptions.options(), collectionName);
return findAndReplace(query, replacement, FindAndReplaceOptions.empty(), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> 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 { @@ -945,13 +951,16 @@ public interface MongoOperations extends FluentMongoOperations {
* @since 2.1
*/
@Nullable
<T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options);
default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
return findAndReplace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement)));
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> 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 { @@ -963,19 +972,24 @@ public interface MongoOperations extends FluentMongoOperations {
* @since 2.1
*/
@Nullable
<T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName);
default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
Assert.notNull(replacement, "Replacement must not be null!");
return findAndReplace(query, replacement, options, (Class<T>) ClassUtils.getUserClass(replacement), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> 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 { @@ -983,8 +997,63 @@ public interface MongoOperations extends FluentMongoOperations {
* @since 2.1
*/
@Nullable
<T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityClass,
String collectionName);
default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityType,
String collectionName) {
return findAndReplace(query, replacement, options, entityType, collectionName, entityType);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> 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 <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
Class<T> resultType) {
return findAndReplace(query, replacement, options, entityType,
getCollectionName(ClassUtils.getUserClass(entityType)), resultType);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> 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
<S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
String collectionName, Class<T> 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

130
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

@ -1072,60 +1072,33 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -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> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
Assert.notNull(replacement, "Replacement must not be null!");
Class<T> entityClass = (Class) ClassUtils.getUserClass(replacement);
String collectionName = determineCollectionName(entityClass);
return findAndReplace(query, replacement, options, collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions, java.lang.String)
*/
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
Assert.notNull(replacement, "Replacement must not be null!");
Class<T> entityClass = (Class) ClassUtils.getUserClass(replacement);
return findAndReplace(query, replacement, options, entityClass, collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions, java.lang.Class, java.lang.String)
*/
@Override
public <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityClass,
String collectionName) {
public <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
String collectionName, Class<T> 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, @@ -2661,30 +2634,38 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
new ReadDocumentCallback<>(readerToUse, entityClass, collectionName), collectionName);
}
protected <T> T doFindAndReplace(String collectionName, Document query, Document fields, Document sort,
Class<T> entityClass, Object replacement, @Nullable FindAndReplaceOptions options) {
EntityReader<? super T, Bson> readerToUse = this.mongoConverter;
if (options == null) {
options = new FindAndReplaceOptions();
}
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
Document mappedQuery = queryMapper.getMappedObject(query, entity);
Document dbDoc = toDocument(replacement, this.mongoConverter);
/**
* 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> T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields,
Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class<?> entityType,
Document replacement, FindAndReplaceOptions options, Class<T> 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, @@ -2747,6 +2728,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @param collectionName the collection to be queried
* @return
*/
@Nullable
private <T> T executeFindOneInternal(CollectionCallback<Document> collectionCallback,
DocumentCallback<T> objectCallback, String collectionName) {
@ -3102,37 +3084,53 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -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<Document> {
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<Document> 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);
}
}

101
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; @@ -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 { @@ -692,7 +693,9 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
* document.
* document. <br />
* Options are defaulted to {@link FindAndReplaceOptions#empty()}. <br />
* <strong>NOTE:</strong> 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 { @@ -701,14 +704,16 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* @since 2.1
*/
default <T> Mono<T> findAndReplace(Query query, T replacement) {
return findAndReplace(query, replacement, FindAndReplaceOptions.options());
return findAndReplace(query, replacement, FindAndReplaceOptions.empty());
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
* document.
* document. <br />
* Options are defaulted to {@link FindAndReplaceOptions#empty()}. <br />
* <strong>NOTE:</strong> 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 { @@ -718,14 +723,15 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* @since 2.1
*/
default <T> Mono<T> findAndReplace(Query query, T replacement, String collectionName) {
return findAndReplace(query, replacement, FindAndReplaceOptions.options(), collectionName);
return findAndReplace(query, replacement, FindAndReplaceOptions.empty(), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> 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 { @@ -736,13 +742,16 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* as it is after the update.
* @since 2.1
*/
<T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options);
default <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
return findAndReplace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement)));
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> 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 { @@ -753,27 +762,86 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* as it is after the update.
* @since 2.1
*/
<T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName);
default <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
Assert.notNull(replacement, "Replacement must not be null!");
return findAndReplace(query, replacement, options, (Class<T>) ClassUtils.getUserClass(replacement), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> 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
*/
<T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityClass,
String collectionName);
default <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityType,
String collectionName) {
return findAndReplace(query, replacement, options, entityType, collectionName, entityType);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> 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 <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
Class<T> resultType) {
return findAndReplace(query, replacement, options, entityType,
getCollectionName(ClassUtils.getUserClass(entityType)), resultType);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> 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
*/
<S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
String collectionName, Class<T> 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 { @@ -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);
}

134
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java

@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core; @@ -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; @@ -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 @@ -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 <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
Assert.notNull(replacement, "Replacement must not be null!");
Class<T> entityClass = (Class) ClassUtils.getUserClass(replacement);
String collectionName = determineCollectionName(entityClass);
return findAndReplace(query, replacement, options, collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions, java.lang.String)
*/
@Override
public <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
Assert.notNull(replacement, "Replacement must not be null!");
Class<T> entityClass = (Class) ClassUtils.getUserClass(replacement);
return findAndReplace(query, replacement, options, entityClass, collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAndReplace(org.springframework.data.mongodb.core.query.Query, java.lang.Object, org.springframework.data.mongodb.core.FindAndReplaceOptions, java.lang.Class, java.lang.String)
*/
@Override
public <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityClass,
String collectionName) {
public <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
String collectionName, Class<T> 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 @@ -2481,28 +2473,41 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
});
}
protected <T> Mono<T> doFindAndReplace(String collectionName, Document query, Document fields, Document sort,
Class<T> entityClass, Object replacement, @Nullable FindAndReplaceOptions options) {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
/**
* 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 <T> Mono<T> doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields,
Document mappedSort, com.mongodb.client.model.Collation collation, Class<?> entityType, Document replacement,
FindAndReplaceOptions options, Class<T> 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 @@ -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<Document> {
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<Document> doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
@ -3010,7 +3024,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -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 @@ -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;
}
}

27
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java

@ -72,6 +72,7 @@ public interface ReactiveUpdateOperation { @@ -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<T> {
@ -133,7 +134,7 @@ public interface ReactiveUpdateOperation { @@ -133,7 +134,7 @@ public interface ReactiveUpdateOperation {
* @throws IllegalArgumentException if options is {@literal null}.
* @since 2.1
*/
FindAndReplaceWithOptions<T> replaceWith(T replacement);
FindAndReplaceWithProjection<T> replaceWith(T replacement);
}
/**
@ -187,6 +188,7 @@ public interface ReactiveUpdateOperation { @@ -187,6 +188,7 @@ public interface ReactiveUpdateOperation {
* Define {@link FindAndReplaceOptions}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T> {
@ -198,7 +200,28 @@ public interface ReactiveUpdateOperation { @@ -198,7 +200,28 @@ public interface ReactiveUpdateOperation {
* @return new instance of {@link FindAndReplaceOptions}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
TerminatingFindAndReplace<T> withOptions(FindAndReplaceOptions options);
FindAndReplaceWithProjection<T> withOptions(FindAndReplaceOptions options);
}
/**
* Result type override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface FindAndReplaceWithProjection<T> extends FindAndReplaceWithOptions<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link FindAndReplaceWithProjection}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/
<R> FindAndReplaceWithOptions<R> as(Class<R> resultType);
}
interface ReactiveUpdate<T> extends UpdateWithCollection<T>, UpdateWithQuery<T>, UpdateWithUpdate<T> {}

47
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java

@ -51,22 +51,24 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { @@ -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<T> implements ReactiveUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>,
TerminatingUpdate<T>, FindAndReplaceWithOptions<T>, TerminatingFindAndReplace<T> {
static class ReactiveUpdateSupport<T>
implements ReactiveUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T>,
FindAndReplaceWithOptions<T>, FindAndReplaceWithProjection<T>, TerminatingFindAndReplace<T> {
@NonNull ReactiveMongoTemplate template;
@NonNull Class<T> 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<T> targetType;
/*
* (non-Javadoc)
@ -78,7 +80,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { @@ -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 { @@ -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 { @@ -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 { @@ -131,8 +133,8 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
@Override
public Mono<T> 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 { @@ -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 { @@ -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 { @@ -175,12 +177,12 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
* @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithUpdate#replaceWith(java.lang.Object)
*/
@Override
public FindAndReplaceWithOptions<T> replaceWith(T replacement) {
public FindAndReplaceWithProjection<T> 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 { @@ -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<T> withOptions(FindAndReplaceOptions options) {
public FindAndReplaceWithProjection<T> 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 <R> FindAndReplaceWithOptions<R> as(Class<R> resultType) {
Assert.notNull(resultType, "ResultType must not be null!");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, resultType);
}
private Mono<UpdateResult> doUpdate(boolean multi, boolean upsert) {

14
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java

@ -226,11 +226,23 @@ public class ExecutableUpdateOperationSupportTests { @@ -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()));
}

114
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.*; @@ -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; @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 {

123
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.*; @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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();
}
}

14
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java

@ -197,6 +197,18 @@ public class ReactiveUpdateOperationSupportTests { @@ -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 { @@ -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");

2
src/main/asciidoc/new-features.adoc

@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* <<mongo.sessions, MongoDB 3.6 Session>> support for the imperative and reactive Template APIs.
* <<mongo.transactions, MongoDB 4.0 Transaction>> support and a MongoDB-specific transaction manager implementation.
* <<mongodb.repositories.queries.sort,Default sort specifications for repository query methods>> using `@Query(sort=…)`.
* `findAndReplace` support through imperative and reactive Template APIs.
* <<mongo-template.find-and-replace,findAndReplace>> support through imperative and reactive Template APIs.
[[new-features.2-0-0]]
== What's New in Spring Data MongoDB 2.0

31
src/main/asciidoc/reference/mongodb.adoc

@ -437,7 +437,7 @@ NOTE: Once configured, `MongoTemplate` is thread-safe and can be reused across m @@ -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 "`<<mongo.custom-converters>>`" 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")); @@ -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<User> 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

Loading…
Cancel
Save