Compare commits

...

14 Commits

Author SHA1 Message Date
Christoph Strobl
da7a62c2d0 Reduce method signatures in Reactive-/MongoOperations and add fluent reactive api variant 2023-08-31 14:08:25 +02:00
Mark Paluch
e14e1f57db Guard against potential NPE. 2023-08-31 11:58:26 +02:00
Mark Paluch
86d289c862 Polishing.
Consistently use Document instead of record. Reformat code. Tweak documentation wording.
2023-08-31 11:58:26 +02:00
Christoph Strobl
8d56a63016 Aggregation update operators cannot be be used here - remove the leftover
should we fail right away if we encounter an aggregation update?
2023-08-31 11:58:26 +02:00
Christoph Strobl
e206d12f5c apply write concern 2023-08-31 11:58:26 +02:00
Christoph Strobl
a4edb6cacc Tests for reactive API 2023-08-31 11:58:26 +02:00
Christoph Strobl
d2a0e739e8 some more updates 2023-08-31 11:58:25 +02:00
Christoph Strobl
53669b9e50 update javadoc and make sure to have a fluent variant as well 2023-08-31 11:58:25 +02:00
Christoph Strobl
f21ca57dcd move stuff into place 2023-08-31 11:58:25 +02:00
Christoph Strobl
9c4072359a moar hacking 2023-08-31 11:58:25 +02:00
Christoph Strobl
0eccd2794d let's have some tests 2023-08-31 11:58:25 +02:00
Christoph Strobl
1762491714 Update a bit of java doc 2023-08-31 11:58:24 +02:00
Jakub
f60c529334 Add support for replaceOne operation 2023-08-31 11:58:24 +02:00
Christoph Strobl
ce140e3deb Prepare issue branch. 2023-08-31 11:58:24 +02:00
23 changed files with 1464 additions and 135 deletions

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.0-SNAPSHOT</version>
<version>4.2.x-4462-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.0-SNAPSHOT</version>
<version>4.2.x-4462-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.0-SNAPSHOT</version>
<version>4.2.x-4462-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.0-SNAPSHOT</version>
<version>4.2.x-4462-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -87,6 +87,23 @@ public interface ExecutableUpdateOperation {
T findAndModifyValue();
}
/**
* Trigger <a href="https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/">replaceOne</a>
* execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @since 4.2
*/
interface TerminatingReplace {
/**
* Find first and replace/upsert.
*
* @return never {@literal null}.
*/
UpdateResult replaceFirst();
}
/**
* Trigger
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace</a>
@@ -95,7 +112,7 @@ public interface ExecutableUpdateOperation {
* @author Mark Paluch
* @since 2.1
*/
interface TerminatingFindAndReplace<T> {
interface TerminatingFindAndReplace<T> extends TerminatingReplace {
/**
* Find, replace and return the first matching document.
@@ -243,6 +260,22 @@ public interface ExecutableUpdateOperation {
TerminatingFindAndModify<T> withOptions(FindAndModifyOptions options);
}
/**
* @author Christoph Strobl
* @since 4.2
*/
interface ReplaceWithOptions extends TerminatingReplace {
/**
* Explicitly define {@link ReplaceOptions}.
*
* @param options must not be {@literal null}.
* @return new instance of {@link FindAndReplaceOptions}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
TerminatingReplace withOptions(ReplaceOptions options);
}
/**
* Define {@link FindAndReplaceOptions}.
*
@@ -250,7 +283,7 @@ public interface ExecutableUpdateOperation {
* @author Christoph Strobl
* @since 2.1
*/
interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T> {
interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T>, ReplaceWithOptions {
/**
* Explicitly define {@link FindAndReplaceOptions} for the {@link Update}.

View File

@@ -126,6 +126,17 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
options, replacement, targetType);
}
@Override
public TerminatingReplace withOptions(ReplaceOptions options) {
FindAndReplaceOptions target = new FindAndReplaceOptions();
if (options.isUpsert()) {
target.upsert();
}
return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
target, replacement, targetType);
}
@Override
public UpdateWithUpdate<T> matching(Query query) {
@@ -175,6 +186,18 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
getCollectionName(), targetType);
}
@Override
public UpdateResult replaceFirst() {
if (replacement != null) {
return template.replace(query, domainType, replacement,
findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName());
}
return template.replace(query, domainType, update,
findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName());
}
private UpdateResult doUpdate(boolean multi, boolean upsert) {
return template.doUpdate(getCollectionName(), query, update, domainType, upsert, multi);
}

View File

@@ -31,10 +31,9 @@ package org.springframework.data.mongodb.core;
* @author Christoph Strobl
* @since 2.1
*/
public class FindAndReplaceOptions {
public class FindAndReplaceOptions extends ReplaceOptions {
private boolean returnNew;
private boolean upsert;
private static final FindAndReplaceOptions NONE = new FindAndReplaceOptions() {
@@ -109,7 +108,7 @@ public class FindAndReplaceOptions {
*/
public FindAndReplaceOptions upsert() {
this.upsert = true;
super.upsert();
return this;
}
@@ -122,13 +121,4 @@ public class FindAndReplaceOptions {
return returnNew;
}
/**
* Get the bit indicating if to create a new document if not exists.
*
* @return {@literal true} if set.
*/
public boolean isUpsert() {
return upsert;
}
}

View File

@@ -21,9 +21,10 @@ package org.springframework.data.mongodb.core;
*
* @author Mark Pollack
* @author Oliver Gierke
* @author Christoph Strobl
* @see MongoAction
*/
public enum MongoActionOperation {
REMOVE, UPDATE, INSERT, INSERT_LIST, SAVE, BULK;
REMOVE, UPDATE, INSERT, INSERT_LIST, SAVE, BULK, REPLACE;
}

View File

@@ -40,6 +40,7 @@ import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.mapreduce.MapReduceResults;
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.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
@@ -118,7 +119,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Execute a MongoDB query and iterate over the query results on a per-document basis with a DocumentCallbackHandler.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param collectionName name of the collection to retrieve the objects from.
* @param dch the handler that will extract results, one document at a time.
@@ -224,7 +225,7 @@ public interface MongoOperations extends FluentMongoOperations {
* <p>
* Returns a {@link String} that wraps the Mongo DB {@link com.mongodb.client.FindIterable} that needs to be closed.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityType must not be {@literal null}.
* @param <T> element return type
@@ -240,7 +241,7 @@ public interface MongoOperations extends FluentMongoOperations {
* <p>
* Returns a {@link Stream} that wraps the Mongo DB {@link com.mongodb.client.FindIterable} that needs to be closed.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityType must not be {@literal null}.
* @param collectionName must not be {@literal null} or empty.
@@ -722,7 +723,7 @@ public interface MongoOperations extends FluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned list.
* @return the converted object.
@@ -738,7 +739,7 @@ public interface MongoOperations extends FluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned list.
* @param collectionName name of the collection to retrieve the objects from.
@@ -752,7 +753,7 @@ public interface MongoOperations extends FluentMongoOperations {
* <strong>NOTE:</strong> Any additional support for query/field mapping, etc. is not available due to the lack of
* domain type information. Use {@link #exists(Query, Class, String)} to get full type specific support.
*
* @param query the {@link Query} class that specifies the criteria used to find a record.
* @param query the {@link Query} class that specifies the criteria used to find a document.
* @param collectionName name of the collection to check for objects.
* @return {@literal true} if the query yields a result.
*/
@@ -761,7 +762,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Determine result of given {@link Query} contains at least one element.
*
* @param query the {@link Query} class that specifies the criteria used to find a record.
* @param query the {@link Query} class that specifies the criteria used to find a document.
* @param entityClass the parametrized type.
* @return {@literal true} if the query yields a result.
*/
@@ -770,7 +771,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Determine result of given {@link Query} contains at least one element.
*
* @param query the {@link Query} class that specifies the criteria used to find a record.
* @param query the {@link Query} class that specifies the criteria used to find a document.
* @param entityClass the parametrized type. Can be {@literal null}.
* @param collectionName name of the collection to check for objects.
* @return {@literal true} if the query yields a result.
@@ -784,7 +785,7 @@ public interface MongoOperations extends FluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityClass the parametrized type of the returned list. Must not be {@literal null}.
* @return the List of converted objects.
@@ -798,7 +799,7 @@ public interface MongoOperations extends FluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityClass the parametrized type of the returned list. Must not be {@literal null}.
* @param collectionName name of the collection to retrieve the objects from. Must not be {@literal null}.
@@ -819,7 +820,7 @@ public interface MongoOperations extends FluentMongoOperations {
* sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
* {@code null} values through {@code $gt/$lt} operators.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityType the parametrized type of the returned window.
* @return the converted window.
@@ -844,7 +845,7 @@ public interface MongoOperations extends FluentMongoOperations {
* sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
* {@code null} values through {@code $gt/$lt} operators.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityType the parametrized type of the returned window.
* @param collectionName name of the collection to retrieve the objects from.
@@ -942,7 +943,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify </a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}.
* @param entityClass the parametrized type. Must not be {@literal null}.
@@ -958,7 +959,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify </a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}.
* @param entityClass the parametrized type. Must not be {@literal null}.
@@ -976,7 +977,7 @@ public interface MongoOperations extends FluentMongoOperations {
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification.
* @param update the {@link UpdateDefinition} to apply on matching documents.
* @param options the {@link FindAndModifyOptions} holding additional information.
@@ -996,7 +997,7 @@ public interface MongoOperations extends FluentMongoOperations {
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
@@ -1022,7 +1023,7 @@ public interface MongoOperations extends FluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @return the converted object that was updated or {@literal null}, if not found.
@@ -1043,7 +1044,7 @@ public interface MongoOperations extends FluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
@@ -1062,7 +1063,7 @@ public interface MongoOperations extends FluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -1085,7 +1086,7 @@ public interface MongoOperations extends FluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -1108,7 +1109,7 @@ public interface MongoOperations extends FluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -1133,7 +1134,7 @@ public interface MongoOperations extends FluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -1163,7 +1164,7 @@ public interface MongoOperations extends FluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -1188,7 +1189,7 @@ public interface MongoOperations extends FluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned list.
* @return the converted object
@@ -1205,7 +1206,7 @@ public interface MongoOperations extends FluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned list.
* @param collectionName name of the collection to retrieve the objects from.
@@ -1490,7 +1491,7 @@ public interface MongoOperations extends FluentMongoOperations {
* <strong>NOTE:</strong> {@link Query#getSortObject() sorting} is not supported by {@code db.collection.updateOne}.
* Use {@link #findAndModify(Query, UpdateDefinition, FindAndModifyOptions, Class, String)} instead.
*
* @param query the query document that specifies the criteria used to select a record to be upserted. Must not be
* @param query the query document that specifies the criteria used to select a document to be upserted. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing object. Must not be {@literal null}.
@@ -1513,7 +1514,7 @@ public interface MongoOperations extends FluentMongoOperations {
* <strong>NOTE:</strong> {@link Query#getSortObject() sorting} is not supported by {@code db.collection.updateOne}.
* Use {@link #findAndModify(Query, UpdateDefinition, FindAndModifyOptions, Class, String)} instead.
*
* @param query the query document that specifies the criteria used to select a record to be upserted. Must not be
* @param query the query document that specifies the criteria used to select a document to be upserted. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing object. Must not be {@literal null}.
@@ -1529,7 +1530,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Performs an upsert. If no document is found that matches the query, a new document is created and inserted by
* combining the query document and the update document.
*
* @param query the query document that specifies the criteria used to select a record to be upserted. Must not be
* @param query the query document that specifies the criteria used to select a document to be upserted. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing object. Must not be {@literal null}.
@@ -1546,7 +1547,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Updates the first object that is found in the collection of the entity class that matches the query document with
* the provided update document.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1569,7 +1570,7 @@ public interface MongoOperations extends FluentMongoOperations {
* <strong>NOTE:</strong> {@link Query#getSortObject() sorting} is not supported by {@code db.collection.updateOne}.
* Use {@link #findAndModify(Query, UpdateDefinition, Class, String)} instead.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1585,7 +1586,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Updates the first object that is found in the specified collection that matches the query document criteria with
* the provided updated document. <br />
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1602,7 +1603,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Updates all objects that are found in the collection for the entity class that matches the query document criteria
* with the provided updated document.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1623,7 +1624,7 @@ public interface MongoOperations extends FluentMongoOperations {
* domain type information. Use {@link #updateMulti(Query, UpdateDefinition, Class, String)} to get full type specific
* support.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1639,7 +1640,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Updates all objects that are found in the collection for the entity class that matches the query document criteria
* with the provided updated document.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1672,7 +1673,7 @@ public interface MongoOperations extends FluentMongoOperations {
* acknowledged} remove operation was successful or not.
*
* @param object must not be {@literal null}.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
*/
DeleteResult remove(Object object, String collectionName);
@@ -1681,7 +1682,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Remove all documents that match the provided query document criteria from the collection used to store the
* entityClass. The Class parameter is also used to help convert the Id of the object if it is present in the query.
*
* @param query the query document that specifies the criteria used to remove a record.
* @param query the query document that specifies the criteria used to remove a document.
* @param entityClass class that determines the collection to use.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
* @throws IllegalArgumentException when {@literal query} or {@literal entityClass} is {@literal null}.
@@ -1694,9 +1695,9 @@ public interface MongoOperations extends FluentMongoOperations {
* Remove all documents that match the provided query document criteria from the collection used to store the
* entityClass. The Class parameter is also used to help convert the Id of the object if it is present in the query.
*
* @param query the query document that specifies the criteria used to remove a record.
* @param query the query document that specifies the criteria used to remove a document.
* @param entityClass class of the pojo to be operated on. Can be {@literal null}.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
* @throws IllegalArgumentException when {@literal query}, {@literal entityClass} or {@literal collectionName} is
* {@literal null}.
@@ -1709,8 +1710,8 @@ public interface MongoOperations extends FluentMongoOperations {
* <strong>NOTE:</strong> Any additional support for field mapping is not available due to the lack of domain type
* information. Use {@link #remove(Query, Class, String)} to get full type specific support.
*
* @param query the query document that specifies the criteria used to remove a record.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param query the query document that specifies the criteria used to remove a document.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
* @throws IllegalArgumentException when {@literal query} or {@literal collectionName} is {@literal null}.
*/
@@ -1722,7 +1723,7 @@ public interface MongoOperations extends FluentMongoOperations {
* information. Use {@link #findAllAndRemove(Query, Class, String)} to get full type specific support.
*
* @param query the query document that specifies the criteria used to find and remove documents.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link List} converted objects deleted by this operation.
* @since 1.5
*/
@@ -1747,12 +1748,79 @@ public interface MongoOperations extends FluentMongoOperations {
*
* @param query the query document that specifies the criteria used to find and remove documents.
* @param entityClass class of the pojo to be operated on.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link List} converted objects deleted by this operation.
* @since 1.5
*/
<T> List<T> findAllAndRemove(Query query, Class<T> entityClass, String collectionName);
/**
* Replace a single document matching the {@link Criteria} of given {@link Query} with the {@code replacement}
* document. <br />
* The collection name is derived from the {@literal replacement} type. <br />
* Options are defaulted to {@link ReplaceOptions#none()}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document. The query may
* contain an index {@link Query#withHint(String) hint} or the {@link Query#collation(Collation) collation}
* to use. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
* {@link #getCollectionName(Class) derived} from the given replacement value.
* @since 4.2
*/
default <T> UpdateResult replace(Query query, T replacement) {
return replace(query, replacement, ReplaceOptions.none());
}
/**
* Replace a single document matching the {@link Criteria} of given {@link Query} with the {@code replacement}
* document. Options are defaulted to {@link ReplaceOptions#none()}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document. The query may
* contain an index {@link Query#withHint(String) hint} or the {@link Query#collation(Collation) collation}
* to use. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
* @since 4.2
*/
default <T> UpdateResult replace(Query query, T replacement, String collectionName) {
return replace(query, replacement, ReplaceOptions.none(), collectionName);
}
/**
* Replace a single document matching the {@link Criteria} of given {@link Query} with the {@code replacement}
* document taking {@link ReplaceOptions} into account.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document.The query may
* contain an index {@link Query#withHint(String) hint} or the {@link Query#collation(Collation) collation}
* to use. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link ReplaceOptions} holding additional information. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
* {@link #getCollectionName(Class) derived} from the given replacement value.
* @since 4.2
*/
default <T> UpdateResult replace(Query query, T replacement, ReplaceOptions options) {
return replace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement)));
}
/**
* Replace a single document matching the {@link Criteria} of given {@link Query} with the {@code replacement}
* document taking {@link ReplaceOptions} into account.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document. The query may *
* contain an index {@link Query#withHint(String) hint} or the {@link Query#collation(Collation) collation}
* to use. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link ReplaceOptions} holding additional information. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
* @since 4.2
*/
<T> UpdateResult replace(Query query, T replacement, ReplaceOptions options, String collectionName);
/**
* Returns the underlying {@link MongoConverter}.
*

View File

@@ -178,6 +178,7 @@ import com.mongodb.client.result.UpdateResult;
* @author Anton Barkan
* @author Bartłomiej Mazur
* @author Michael Krog
* @author Jakub Zurawa
*/
public class MongoTemplate
implements MongoOperations, ApplicationContextAware, IndexOperationsProvider, ReadPreferenceAware {
@@ -1310,7 +1311,7 @@ public class MongoTemplate
if (ObjectUtils.nullSafeEquals(WriteResultChecking.EXCEPTION, writeResultChecking)) {
if (wc == null || wc.getWObject() == null
|| (wc.getWObject() instanceof Number concern && concern.intValue() < 1)) {
|| (wc.getWObject()instanceof Number concern && concern.intValue() < 1)) {
return WriteConcern.ACKNOWLEDGED;
}
}
@@ -1618,7 +1619,7 @@ public class MongoTemplate
}
}
collectionToUse.replaceOne(filter, replacement, new ReplaceOptions().upsert(true));
collectionToUse.replaceOne(filter, replacement, new com.mongodb.client.model.ReplaceOptions().upsert(true));
}
return mapped.getId();
});
@@ -1749,7 +1750,7 @@ public class MongoTemplate
}
}
ReplaceOptions replaceOptions = updateContext.getReplaceOptions(entityClass);
com.mongodb.client.model.ReplaceOptions replaceOptions = updateContext.getReplaceOptions(entityClass);
return collection.replaceOne(filter, updateObj, replaceOptions);
} else {
return multi ? collection.updateMany(queryObj, updateObj, opts)
@@ -2067,6 +2068,48 @@ public class MongoTemplate
return doFindAndDelete(collectionName, query, entityClass);
}
@Override
public <T> UpdateResult replace(Query query, T replacement, ReplaceOptions options, String collectionName){
Assert.notNull(replacement, "Replacement must not be null");
return replace(query, (Class<T>) ClassUtils.getUserClass(replacement), replacement, options, collectionName);
}
protected <S, T> UpdateResult replace(Query query, Class<S> entityType, T replacement, ReplaceOptions options,
String collectionName) {
Assert.notNull(query, "Query must not be null");
Assert.notNull(replacement, "Replacement must not be null");
Assert.notNull(options, "Options must not be null Use ReplaceOptions#none() instead");
Assert.notNull(entityType, "EntityType must not be null");
Assert.notNull(collectionName, "CollectionName must not be null");
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");
UpdateContext updateContext = queryOperations.replaceSingleContext(query,
operations.forEntity(replacement).toMappedDocument(this.mongoConverter), options.isUpsert());
replacement = maybeCallBeforeConvert(replacement, collectionName);
Document mappedReplacement = updateContext.getMappedUpdate(mappingContext.getPersistentEntity(entityType));
maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName));
replacement = maybeCallBeforeSave(replacement, mappedReplacement, collectionName);
MongoAction action = new MongoAction(writeConcern, MongoActionOperation.REPLACE, collectionName, entityType,
mappedReplacement, updateContext.getQueryObject());
UpdateResult result = doReplace(options, entityType, collectionName, updateContext,
createCollectionPreparer(query, action), mappedReplacement);
if (result.wasAcknowledged()) {
maybeEmitEvent(new AfterSaveEvent<>(replacement, mappedReplacement, collectionName));
maybeCallAfterSave(replacement, mappedReplacement, collectionName);
}
return result;
}
/**
* Retrieve and remove all documents matching the given {@code query} by calling {@link #find(Query, Class, String)}
* and {@link #remove(Query, Class, String)}, whereas the {@link Query} for {@link #remove(Query, Class, String)} is
@@ -2732,6 +2775,17 @@ public class MongoTemplate
return CollectionPreparerDelegate.of(query);
}
CollectionPreparer<MongoCollection<Document>> createCollectionPreparer(Query query, @Nullable MongoAction action) {
CollectionPreparer<MongoCollection<Document>> collectionPreparer = createDelegate(query);
if (action == null) {
return collectionPreparer;
}
return collectionPreparer.andThen(collection -> {
WriteConcern writeConcern = prepareWriteConcern(action);
return writeConcern != null ? collection.withWriteConcern(writeConcern) : collection;
});
}
/**
* Customize this part for findAndReplace.
*
@@ -2767,6 +2821,24 @@ public class MongoTemplate
collectionName);
}
private <S> UpdateResult doReplace(ReplaceOptions options, Class<S> entityType, String collectionName,
UpdateContext updateContext, CollectionPreparer<MongoCollection<Document>> collectionPreparer,
Document replacement) {
MongoPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(entityType);
ReplaceCallback replaceCallback = new ReplaceCallback(collectionPreparer,
updateContext.getMappedQuery(persistentEntity), replacement, updateContext.getReplaceOptions(entityType, it -> {
it.upsert(options.isUpsert());
}));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("replace one using query: %s for class: %s in collection: %s",
serializeToJsonSafely(updateContext.getMappedQuery(persistentEntity)), entityType, collectionName));
}
return execute(collectionName, replaceCallback);
}
/**
* Populates the id property of the saved object, if it's not set already.
*
@@ -3555,4 +3627,26 @@ public class MongoTemplate
long countDocuments(CollectionPreparer collectionPreparer, String collection, Document filter,
CountOptions options);
}
private static class ReplaceCallback implements CollectionCallback<UpdateResult> {
private final CollectionPreparer<MongoCollection<Document>> collectionPreparer;
private final Document query;
private final Document update;
private final com.mongodb.client.model.ReplaceOptions options;
ReplaceCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document update,
com.mongodb.client.model.ReplaceOptions options) {
this.collectionPreparer = collectionPreparer;
this.query = query;
this.update = update;
this.options = options;
}
@Override
public UpdateResult doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
return collectionPreparer.prepare(collection).replaceOne(query, update, options);
}
}
}

View File

@@ -193,6 +193,15 @@ class QueryOperations {
return new UpdateContext(replacement, upsert);
}
/**
* @param replacement the {@link MappedDocument mapped replacement} document.
* @param upsert use {@literal true} to insert diff when no existing document found.
* @return new instance of {@link UpdateContext}.
*/
UpdateContext replaceSingleContext(Query query, MappedDocument replacement, boolean upsert) {
return new UpdateContext(query, replacement, upsert);
}
/**
* Create a new {@link DeleteContext} instance removing all matching documents.
*
@@ -439,6 +448,25 @@ class QueryOperations {
return entityOperations.forType(domainType).getCollation(query) //
.map(Collation::toMongoCollation);
}
/**
* Get the {@link HintFunction} reading the actual hint form the {@link Query}.
*
* @return new instance of {@link HintFunction}.
* @since 4.2
*/
HintFunction getHintFunction() {
return HintFunction.from(query.getHint());
}
/**
* Read and apply the hint from the {@link Query}.
*
* @since 4.2
*/
<R> void applyHint(Function<String, R> stringConsumer, Function<Bson, R> bsonConsumer) {
getHintFunction().ifPresent(codecRegistryProvider, stringConsumer, bsonConsumer);
}
}
/**
@@ -696,8 +724,12 @@ class QueryOperations {
}
UpdateContext(MappedDocument update, boolean upsert) {
this(new BasicQuery(BsonUtils.asDocument(update.getIdFilter())), update, upsert);
}
super(new BasicQuery(BsonUtils.asDocument(update.getIdFilter())));
UpdateContext(Query query, MappedDocument update, boolean upsert) {
super(query);
this.multi = false;
this.upsert = upsert;
this.mappedDocument = update;
@@ -765,6 +797,7 @@ class QueryOperations {
ReplaceOptions options = new ReplaceOptions();
options.collation(updateOptions.getCollation());
options.upsert(updateOptions.isUpsert());
applyHint(options::hintString, options::hint);
if (callback != null) {
callback.accept(options);
@@ -778,7 +811,7 @@ class QueryOperations {
Document mappedQuery = super.getMappedQuery(domainType);
if (multi && update.isIsolated() && !mappedQuery.containsKey("$isolated")) {
if (multi && update != null && update.isIsolated() && !mappedQuery.containsKey("$isolated")) {
mappedQuery.put("$isolated", 1);
}
@@ -874,7 +907,7 @@ class QueryOperations {
if (persistentEntity != null && persistentEntity.hasVersionProperty()) {
String versionFieldName = persistentEntity.getRequiredVersionProperty().getFieldName();
if (!update.modifies(versionFieldName)) {
if (update != null && !update.modifies(versionFieldName)) {
update.inc(versionFieldName);
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core;
import org.springframework.data.mongodb.core.query.Collation;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -417,7 +418,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned {@link Mono}.
* @return the converted object.
@@ -432,7 +433,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned {@link Mono}.
* @param collectionName name of the collection to retrieve the objects from.
@@ -445,7 +446,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* <strong>NOTE:</strong> Any additional support for query/field mapping, etc. is not available due to the lack of
* domain type information. Use {@link #exists(Query, Class, String)} to get full type specific support.
*
* @param query the {@link Query} class that specifies the criteria used to find a record.
* @param query the {@link Query} class that specifies the criteria used to find a document.
* @param collectionName name of the collection to check for objects.
* @return {@literal true} if the query yields a result.
*/
@@ -454,7 +455,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Determine result of given {@link Query} contains at least one element.
*
* @param query the {@link Query} class that specifies the criteria used to find a record.
* @param query the {@link Query} class that specifies the criteria used to find a document.
* @param entityClass the parametrized type.
* @return {@literal true} if the query yields a result.
*/
@@ -463,7 +464,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Determine result of given {@link Query} contains at least one element.
*
* @param query the {@link Query} class that specifies the criteria used to find a record.
* @param query the {@link Query} class that specifies the criteria used to find a document.
* @param entityClass the parametrized type. Can be {@literal null}.
* @param collectionName name of the collection to check for objects.
* @return {@literal true} if the query yields a result.
@@ -478,7 +479,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityClass the parametrized type of the returned {@link Flux}. Must not be {@literal null}.
* @return the {@link Flux} of converted objects.
@@ -492,7 +493,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityClass the parametrized type of the returned {@link Flux}.
* @param collectionName name of the collection to retrieve the objects from. Must not be {@literal null}.
@@ -513,7 +514,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
* {@code null} values through {@code $gt/$lt} operators.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityType the parametrized type of the returned list.
* @return {@link Mono} emitting the converted window.
@@ -538,7 +539,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
* {@code null} values through {@code $gt/$lt} operators.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification. Must not be {@literal null}.
* @param entityType the parametrized type of the returned list.
* @param collectionName name of the collection to retrieve the objects from.
@@ -758,7 +759,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify</a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}.
* @param entityClass the parametrized type. Must not be {@literal null}.
@@ -773,7 +774,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify</a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}.
* @param entityClass the parametrized type. Must not be {@literal null}.
@@ -790,7 +791,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification.
* @param update the {@link UpdateDefinition} to apply on matching documents.
* @param options the {@link FindAndModifyOptions} holding additional information.
@@ -808,7 +809,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param update the {@link UpdateDefinition} to apply on matching documents. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
@@ -831,7 +832,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @return the converted object that was updated or {@link Mono#empty()}, if not found.
@@ -851,7 +852,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
@@ -869,7 +870,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -891,7 +892,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -913,7 +914,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -937,7 +938,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -966,7 +967,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document 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}.
@@ -991,7 +992,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned {@link Mono}.
* @return the converted object
@@ -1007,7 +1008,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned {@link Mono}.
* @param collectionName name of the collection to retrieve the objects from.
@@ -1369,7 +1370,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* <strong>NOTE:</strong> {@link Query#getSortObject() sorting} is not supported by {@code db.collection.updateOne}.
* Use {@link #findAndModify(Query, UpdateDefinition, Class)} instead.
*
* @param query the query document that specifies the criteria used to select a record to be upserted. Must not be
* @param query the query document that specifies the criteria used to select a document to be upserted. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing object. Must not be {@literal null}.
@@ -1390,7 +1391,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* domain type information. Use {@link #upsert(Query, UpdateDefinition, Class, String)} to get full type specific
* support.
*
* @param query the query document that specifies the criteria used to select a record to be upserted. Must not be
* @param query the query document that specifies the criteria used to select a document to be upserted. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing object. Must not be {@literal null}.
@@ -1406,7 +1407,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Performs an upsert. If no document is found that matches the query, a new document is created and inserted by
* combining the query document and the update document.
*
* @param query the query document that specifies the criteria used to select a record to be upserted. Must not be
* @param query the query document that specifies the criteria used to select a document to be upserted. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing object. Must not be {@literal null}.
@@ -1425,7 +1426,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* <strong>NOTE:</strong> {@link Query#getSortObject() sorting} is not supported by {@code db.collection.updateOne}.
* Use {@link #findAndModify(Query, UpdateDefinition, Class)} instead.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1448,7 +1449,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* <strong>NOTE:</strong> {@link Query#getSortObject() sorting} is not supported by {@code db.collection.updateOne}.
* Use {@link #findAndModify(Query, UpdateDefinition, Class, String)} instead.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1464,7 +1465,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Updates the first object that is found in the specified collection that matches the query document criteria with
* the provided updated document. <br />
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1481,7 +1482,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Updates all objects that are found in the collection for the entity class that matches the query document criteria
* with the provided updated document.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1502,7 +1503,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* domain type information. Use {@link #updateMulti(Query, UpdateDefinition, Class, String)} to get full type specific
* support.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1518,7 +1519,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Updates all objects that are found in the collection for the entity class that matches the query document criteria
* with the provided updated document.
*
* @param query the query document that specifies the criteria used to select a record to be updated. Must not be
* @param query the query document that specifies the criteria used to select a document to be updated. Must not be
* {@literal null}.
* @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}.
@@ -1545,7 +1546,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Removes the given object from the given collection.
*
* @param object must not be {@literal null}.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
*/
Mono<DeleteResult> remove(Object object, String collectionName);
@@ -1564,7 +1565,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Removes the given object from the given collection.
*
* @param objectToRemove must not be {@literal null}.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
*/
Mono<DeleteResult> remove(Mono<? extends Object> objectToRemove, String collectionName);
@@ -1573,7 +1574,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Remove all documents that match the provided query document criteria from the collection used to store the
* entityClass. The Class parameter is also used to help convert the Id of the object if it is present in the query.
*
* @param query the query document that specifies the criteria used to remove a record.
* @param query the query document that specifies the criteria used to remove a document.
* @param entityClass class that determines the collection to use.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
* @throws org.springframework.data.mapping.MappingException if the target collection name cannot be
@@ -1585,9 +1586,9 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Remove all documents that match the provided query document criteria from the collection used to store the
* entityClass. The Class parameter is also used to help convert the Id of the object if it is present in the query.
*
* @param query the query document that specifies the criteria used to remove a record.
* @param query the query document that specifies the criteria used to remove a document.
* @param entityClass class of the pojo to be operated on. Can be {@literal null}.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
*/
Mono<DeleteResult> remove(Query query, @Nullable Class<?> entityClass, String collectionName);
@@ -1598,8 +1599,8 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* <strong>NOTE:</strong> Any additional support for field mapping is not available due to the lack of domain type
* information. Use {@link #remove(Query, Class, String)} to get full type specific support.
*
* @param query the query document that specifies the criteria used to remove a record.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param query the query document that specifies the criteria used to remove a document.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
*/
Mono<DeleteResult> remove(Query query, String collectionName);
@@ -1610,7 +1611,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* information. Use {@link #findAllAndRemove(Query, Class, String)} to get full type specific support.
*
* @param query the query document that specifies the criteria used to find and remove documents.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link Flux} converted objects deleted by this operation.
*/
<T> Flux<T> findAllAndRemove(Query query, String collectionName);
@@ -1633,11 +1634,80 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @param query the query document that specifies the criteria used to find and remove documents.
* @param entityClass class of the pojo to be operated on.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @param collectionName name of the collection where the documents will be removed from, must not be {@literal null} or empty.
* @return the {@link Flux} converted objects deleted by this operation.
*/
<T> Flux<T> findAllAndRemove(Query query, Class<T> entityClass, String collectionName);
/**
* Replace a single document matching the {@link Criteria} of given {@link Query} with the {@code replacement}
* document. <br />
* The collection name is derived from the {@literal replacement} type. <br />
* Options are defaulted to {@link ReplaceOptions#none()}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document. The query may
* contain an index {@link Query#withHint(String) hint} or the {@link Query#collation(Collation) collation}
* to use. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
* {@link #getCollectionName(Class) derived} from the given replacement value.
* @since 4.2
*/
default <T> Mono<UpdateResult> replace(Query query, T replacement) {
return replace(query, replacement, ReplaceOptions.none());
}
/**
* Replace a single document matching the {@link Criteria} of given {@link Query} with the {@code replacement}
* document. Options are defaulted to {@link ReplaceOptions#none()}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document. The query may
* contain an index {@link Query#withHint(String) hint} or the {@link Query#collation(Collation) collation}
* to use. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
* @since 4.2
*/
default <T> Mono<UpdateResult> replace(Query query, T replacement, String collectionName) {
return replace(query, replacement, ReplaceOptions.none(), collectionName);
}
/**
* Replace a single document matching the {@link Criteria} of given {@link Query} with the {@code replacement}
* document taking {@link ReplaceOptions} into account.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document.The query may
* contain an index {@link Query#withHint(String) hint} or the {@link Query#collation(Collation) collation}
* to use. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link ReplaceOptions} holding additional information. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
* {@link #getCollectionName(Class) derived} from the given replacement value.
* @since 4.2
*/
default <T> Mono<UpdateResult> replace(Query query, T replacement, ReplaceOptions options) {
return replace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement)));
}
/**
* Replace a single document matching the {@link Criteria} of given {@link Query} with the {@code replacement}
* document taking {@link ReplaceOptions} into account.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a document. The query may *
* contain an index {@link Query#withHint(String) hint} or the {@link Query#collation(Collation) collation}
* to use. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link ReplaceOptions} holding additional information. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
* {@link #getCollectionName(Class) derived} from the given replacement value.
* @since 4.2
*/
<T> Mono<UpdateResult> replace(Query query, T replacement, ReplaceOptions options, String collectionName);
/**
* Map the results of an ad-hoc query on the collection for the entity class to a stream of objects of the specified
* type. The stream uses a {@link com.mongodb.CursorType#TailableAwait tailable} cursor that may be an infinite
@@ -1648,7 +1718,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned {@link Flux}.
* @return the {@link Flux} of converted objects.
@@ -1667,7 +1737,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* @param query the query class that specifies the criteria used to find a document and also an optional fields
* specification.
* @param entityClass the parametrized type of the returned {@link Flux}.
* @param collectionName name of the collection to retrieve the objects from.

View File

@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core;
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
import org.springframework.data.mongodb.core.CollectionPreparerSupport.CollectionPreparerDelegate;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
@@ -1782,7 +1783,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
deferredFilter = Mono.just(filter);
}
ReplaceOptions replaceOptions = updateContext.getReplaceOptions(entityClass);
com.mongodb.client.model.ReplaceOptions replaceOptions = updateContext.getReplaceOptions(entityClass);
return deferredFilter.flatMap(it -> Mono.from(collectionToUse.replaceOne(it, updateObj, replaceOptions)));
}
@@ -1959,6 +1960,34 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return doFindAndDelete(collectionName, query, entityClass);
}
@Override
public <T> Mono<UpdateResult> replace(Query query, T replacement, ReplaceOptions options, String collectionName) {
Assert.notNull(replacement, "Replacement must not be null");
return replace(query, (Class<T>) ClassUtils.getUserClass(replacement), replacement, options, collectionName);
}
protected <S,T> Mono<UpdateResult> replace(Query query, Class<S> entityType, T replacement, ReplaceOptions options,
String collectionName) {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityType);
UpdateContext updateContext = queryOperations.replaceSingleContext(query, operations.forEntity(replacement).toMappedDocument(this.mongoConverter), options.isUpsert());
return createMono(collectionName, collection -> {
Document mappedUpdate = updateContext.getMappedUpdate(entity);
MongoAction action = new MongoAction(writeConcern, MongoActionOperation.REPLACE, collectionName, entityType,
mappedUpdate, updateContext.getQueryObject());
MongoCollection<Document> collectionToUse = createCollectionPreparer(query, action).prepare(collection);
return collectionToUse.replaceOne(updateContext.getMappedQuery(entity), mappedUpdate, updateContext.getReplaceOptions(entityType, it -> {
it.upsert(options.isUpsert());
}));
});
}
@Override
public <T> Flux<T> tail(Query query, Class<T> entityClass) {
return tail(query, entityClass, getCollectionName(entityClass));
@@ -2341,6 +2370,21 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
objectCallback, collectionName);
}
CollectionPreparer<MongoCollection<Document>> createCollectionPreparer(Query query) {
return ReactiveCollectionPreparerDelegate.of(query);
}
CollectionPreparer<MongoCollection<Document>> createCollectionPreparer(Query query, @Nullable MongoAction action) {
CollectionPreparer<MongoCollection<Document>> collectionPreparer = createCollectionPreparer(query);
if (action == null) {
return collectionPreparer;
}
return collectionPreparer.andThen(collection -> {
WriteConcern writeConcern = prepareWriteConcern(action);
return writeConcern != null ? collection.withWriteConcern(writeConcern) : collection;
});
}
/**
* Map the results of an ad-hoc query on the default MongoDB collection to a List of the specified targetClass while
* using sourceClass for mapping the query.

View File

@@ -72,13 +72,30 @@ public interface ReactiveUpdateOperation {
Mono<T> findAndModify();
}
/**
* Trigger <a href="https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/">replaceOne</a>
* execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @since 4.2
*/
interface TerminatingReplace {
/**
* Find first and replace/upsert.
*
* @return never {@literal null}.
*/
Mono<UpdateResult> replaceFirst();
}
/**
* Compose findAndReplace execution by calling one of the terminating methods.
*
* @author Mark Paluch
* @since 2.1
*/
interface TerminatingFindAndReplace<T> {
interface TerminatingFindAndReplace<T> extends TerminatingReplace {
/**
* Find, replace and return the first matching document.
@@ -202,6 +219,22 @@ public interface ReactiveUpdateOperation {
TerminatingFindAndModify<T> withOptions(FindAndModifyOptions options);
}
/**
* @author Christoph Strobl
* @since 4.2
*/
interface ReplaceWithOptions extends TerminatingReplace {
/**
* Explicitly define {@link ReplaceOptions}.
*
* @param options must not be {@literal null}.
* @return new instance of {@link FindAndReplaceOptions}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
TerminatingReplace withOptions(ReplaceOptions options);
}
/**
* Define {@link FindAndReplaceOptions}.
*
@@ -209,7 +242,7 @@ public interface ReactiveUpdateOperation {
* @author Christoph Strobl
* @since 2.1
*/
interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T> {
interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T>, ReplaceWithOptions {
/**
* Explicitly define {@link FindAndReplaceOptions} for the {@link Update}.

View File

@@ -165,6 +165,17 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
replacement, targetType);
}
@Override
public TerminatingReplace withOptions(ReplaceOptions options) {
FindAndReplaceOptions target = new FindAndReplaceOptions();
if (options.isUpsert()) {
target.upsert();
}
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
target, replacement, targetType);
}
@Override
public <R> FindAndReplaceWithOptions<R> as(Class<R> resultType) {
@@ -174,6 +185,18 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
findAndReplaceOptions, replacement, resultType);
}
@Override
public Mono <UpdateResult> replaceFirst() {
if (replacement != null) {
return template.replace(query, domainType, replacement,
findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName());
}
return template.replace(query, domainType, update,
findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName());
}
private Mono<UpdateResult> doUpdate(boolean multi, boolean upsert) {
return template.doUpdate(getCollectionName(), query, update, domainType, upsert, multi);
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import org.springframework.data.mongodb.core.query.Query;
/**
* Options for {@link org.springframework.data.mongodb.core.MongoOperations#replace(Query, Object) replace operations}. Defaults to
* <dl>
* <dt>upsert</dt>
* <dd>false</dd>
* </dl>
*
* @author Jakub Zurawa
* @author Christoph Strob
* @since 4.2
*/
public class ReplaceOptions {
private boolean upsert;
private static final ReplaceOptions NONE = new ReplaceOptions() {
private static final String ERROR_MSG = "ReplaceOptions.none() cannot be changed; Please use ReplaceOptions.options() instead";
@Override
public ReplaceOptions upsert() {
throw new UnsupportedOperationException(ERROR_MSG);
}
};
/**
* Static factory method to create a {@link ReplaceOptions} instance.
* <dl>
* <dt>upsert</dt>
* <dd>false</dd>
* </dl>
*
* @return new instance of {@link ReplaceOptions}.
*/
public static ReplaceOptions replaceOptions() {
return new ReplaceOptions();
}
/**
* Static factory method returning an unmodifiable {@link ReplaceOptions} instance.
*
* @return unmodifiable {@link ReplaceOptions} instance.
*/
public static ReplaceOptions none() {
return NONE;
}
/**
* Insert a new document if not exists.
*
* @return this.
*/
public ReplaceOptions upsert() {
this.upsert = true;
return this;
}
/**
* Get the bit indicating if to create a new document if not exists.
*
* @return {@literal true} if set.
*/
public boolean isUpsert() {
return upsert;
}
}

View File

@@ -247,6 +247,29 @@ class ExecutableUpdateOperationSupportTests {
assertThat(result).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Luke");
}
@Test // GH-4463
void replace() {
Person luke = new Person();
luke.id = han.id;
luke.firstname = "Luke";
UpdateResult result = template.update(Person.class).matching(queryHan()).replaceWith(luke).replaceFirst();
assertThat(result.getModifiedCount()).isEqualTo(1L);
}
@Test // GH-4463
void replaceWithOptions() {
Person luke = new Person();
luke.id = "upserted-luke";
luke.firstname = "Luke";
UpdateResult result = template.update(Person.class).matching(query(where("firstname")
.is("c3p0"))).replaceWith(luke).withOptions(ReplaceOptions.replaceOptions().upsert()).replaceFirst();
assertThat(result.getUpsertedId()).isEqualTo(new BsonString("upserted-luke"));
}
@Test // DATAMONGO-1827
void findAndReplaceWithProjection() {

View File

@@ -0,0 +1,282 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.ReplaceOptions.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bson.BsonInt64;
import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import com.mongodb.client.result.UpdateResult;
/**
* @author Christoph Strobl
*/
@ExtendWith(MongoClientExtension.class)
public class MongoTemplateReplaceTests {
static final String DB_NAME = "mongo-template-replace-tests";
static final String RESTAURANT_COLLECTION = "restaurant";
static @Client MongoClient client;
private MongoTemplate template;
@BeforeEach
void beforeEach() {
template = new MongoTemplate(client, DB_NAME);
template.setEntityLifecycleEventsEnabled(false);
initTestData();
}
@AfterEach()
void afterEach() {
clearTestData();
}
@Test // GH-4462
void replacesExistingDocument() {
UpdateResult result = template.replace(query(where("name").is("Central Perk Cafe")),
new Restaurant("Central Pork Cafe", "Manhattan"));
assertThat(result.getMatchedCount()).isEqualTo(1);
assertThat(result.getModifiedCount()).isEqualTo(1);
Document document = retrieve(collection -> collection.find(Filters.eq("_id", 1)).first());
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}
@Test // GH-4462
void replacesFirstOnMoreThanOneMatch() {
UpdateResult result = template
.replace(query(where("violations").exists(true)), new Restaurant("Central Pork Cafe", "Manhattan"));
assertThat(result.getMatchedCount()).isEqualTo(1);
assertThat(result.getModifiedCount()).isEqualTo(1);
Document document = retrieve(collection -> collection.find(Filters.eq("_id", 2)).first());
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}
@Test // GH-4462
void replacesExistingDocumentWithRawDoc() {
UpdateResult result = template.replace(query(where("r-name").is("Central Perk Cafe")),
Document.parse("{ 'r-name' : 'Central Pork Cafe', 'Borough' : 'Manhattan' }"),
template.getCollectionName(Restaurant.class));
assertThat(result.getMatchedCount()).isEqualTo(1);
assertThat(result.getModifiedCount()).isEqualTo(1);
Document document = retrieve(collection -> collection.find(Filters.eq("_id", 1)).first());
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}
@Test // GH-4462
void replacesExistingDocumentWithRawDocMappingQueryAgainstDomainType() {
UpdateResult result = template.replace(query(where("name").is("Central Perk Cafe")), Restaurant.class,
Document.parse("{ 'r-name' : 'Central Pork Cafe', 'Borough' : 'Manhattan' }"), ReplaceOptions.none(), template.getCollectionName(Restaurant.class));
assertThat(result.getMatchedCount()).isEqualTo(1);
assertThat(result.getModifiedCount()).isEqualTo(1);
Document document = retrieve(collection -> collection.find(Filters.eq("_id", 1)).first());
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}
@Test // GH-4462
void replacesExistingDocumentWithMatchingId() {
UpdateResult result = template.replace(query(where("name").is("Central Perk Cafe")),
new Restaurant(1L, "Central Pork Cafe", "Manhattan", 0));
assertThat(result.getMatchedCount()).isEqualTo(1);
assertThat(result.getModifiedCount()).isEqualTo(1);
Document document = retrieve(collection -> collection.find(Filters.eq("_id", 1)).first());
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}
@Test // GH-4462
void replacesExistingDocumentWithNewIdThrowsDataIntegrityViolationException() {
assertThatExceptionOfType(DataIntegrityViolationException.class)
.isThrownBy(() -> template.replace(query(where("name").is("Central Perk Cafe")),
new Restaurant(4L, "Central Pork Cafe", "Manhattan", 0)));
}
@Test // GH-4462
void doesNothingIfNoMatchFoundAndUpsertSetToFalse/* by default */() {
UpdateResult result = template.replace(query(where("name").is("Pizza Rat's Pizzaria")),
new Restaurant(null, "Pizza Rat's Pizzaria", "Manhattan", 8));
assertThat(result.getMatchedCount()).isEqualTo(0);
assertThat(result.getModifiedCount()).isEqualTo(0);
Document document = retrieve(collection -> collection.find(Filters.eq("r-name", "Pizza Rat's Pizzaria")).first());
assertThat(document).isNull();
}
@Test // GH-4462
void insertsIfNoMatchFoundAndUpsertSetToTrue() {
UpdateResult result = template.replace(query(where("name").is("Pizza Rat's Pizzaria")),
new Restaurant(4L, "Pizza Rat's Pizzaria", "Manhattan", 8), replaceOptions().upsert());
assertThat(result.getMatchedCount()).isEqualTo(0);
assertThat(result.getModifiedCount()).isEqualTo(0);
assertThat(result.getUpsertedId()).isEqualTo(new BsonInt64(4L));
Document document = retrieve(collection -> collection.find(Filters.eq("_id", 4)).first());
assertThat(document).containsEntry("r-name", "Pizza Rat's Pizzaria");
}
void initTestData() {
List<Document> testData = Stream.of( //
"{ '_id' : 1, 'r-name' : 'Central Perk Cafe', 'Borough' : 'Manhattan' }",
"{ '_id' : 2, 'r-name' : 'Rock A Feller Bar and Grill', 'Borough' : 'Queens', 'violations' : 2 }",
"{ '_id' : 3, 'r-name' : 'Empire State Pub', 'Borough' : 'Brooklyn', 'violations' : 0 }") //
.map(Document::parse).collect(Collectors.toList());
doInCollection(collection -> collection.insertMany(testData));
}
void clearTestData() {
doInCollection(collection -> collection.deleteMany(new Document()));
}
void doInCollection(Consumer<MongoCollection<Document>> consumer) {
retrieve(collection -> {
consumer.accept(collection);
return "done";
});
}
<T> T retrieve(Function<MongoCollection<Document>, T> fkt) {
return fkt.apply(client.getDatabase(DB_NAME).getCollection(RESTAURANT_COLLECTION));
}
@org.springframework.data.mongodb.core.mapping.Document(RESTAURANT_COLLECTION)
static class Restaurant {
Long id;
@Field("r-name") String name;
String borough;
Integer violations;
Restaurant() {}
Restaurant(String name, String borough) {
this.name = name;
this.borough = borough;
}
Restaurant(Long id, String name, String borough, Integer violations) {
this.id = id;
this.name = name;
this.borough = borough;
this.violations = violations;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRName() {
return name;
}
public void setRName(String rName) {
this.name = rName;
}
public String getBorough() {
return borough;
}
public void setBorough(String borough) {
this.borough = borough;
}
public int getViolations() {
return violations;
}
public void setViolations(int violations) {
this.violations = violations;
}
@Override
public String toString() {
return "Restaurant{" + "id=" + id + ", name='" + name + '\'' + ", borough='" + borough + '\'' + ", violations="
+ violations + '}';
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Restaurant that = (Restaurant) o;
return violations == that.violations && Objects.equals(id, that.id) && Objects.equals(name, that.name)
&& Objects.equals(borough, that.borough);
}
@Override
public int hashCode() {
return Objects.hash(id, name, borough, violations);
}
}
}

View File

@@ -116,6 +116,7 @@ import com.mongodb.client.result.UpdateResult;
* @author Mark Paluch
* @author Laszlo Csontos
* @author duozhilin
* @author Jakub Zurawa
*/
@ExtendWith(MongoClientExtension.class)
public class MongoTemplateTests {
@@ -3872,6 +3873,21 @@ public class MongoTemplateTests {
assertThat(loaded).isEqualTo(source2);
}
@Test // GH-4300
public void replaceShouldReplaceDocument() {
org.bson.Document doc = new org.bson.Document("foo", "bar");
String collectionName = "replace";
template.save(doc, collectionName);
org.bson.Document replacement = new org.bson.Document("foo", "baz");
UpdateResult updateResult = template.replace(query(where("foo").is("bar")), replacement, ReplaceOptions.replaceOptions(),
collectionName);
assertThat(updateResult.wasAcknowledged()).isTrue();
assertThat(template.findOne(query(where("foo").is("baz")), org.bson.Document.class, collectionName)).isNotNull();
}
private AtomicReference<ImmutableVersioned> createAfterSaveReference() {
AtomicReference<ImmutableVersioned> saved = new AtomicReference<>();

View File

@@ -131,6 +131,7 @@ import com.mongodb.client.result.UpdateResult;
* @author Michael J. Simons
* @author Roman Puchkovskiy
* @author Yadhukrishna S Pai
* @author Jakub Zurawa
*/
@MockitoSettings(strictness = Strictness.LENIENT)
public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@@ -173,10 +174,11 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
when(collection.aggregate(any(List.class), any())).thenReturn(aggregateIterable);
when(collection.withReadConcern(any())).thenReturn(collection);
when(collection.withReadPreference(any())).thenReturn(collection);
when(collection.replaceOne(any(), any(), any(ReplaceOptions.class))).thenReturn(updateResult);
when(collection.replaceOne(any(), any(), any(com.mongodb.client.model.ReplaceOptions.class))).thenReturn(updateResult);
when(collection.withWriteConcern(any())).thenReturn(collectionWithWriteConcern);
when(collection.distinct(anyString(), any(Document.class), any())).thenReturn(distinctIterable);
when(collectionWithWriteConcern.deleteOne(any(Bson.class), any())).thenReturn(deleteResult);
when(collectionWithWriteConcern.replaceOne(any(), any(), any(com.mongodb.client.model.ReplaceOptions.class))).thenReturn(updateResult);
when(findIterable.projection(any())).thenReturn(findIterable);
when(findIterable.sort(any(org.bson.Document.class))).thenReturn(findIterable);
when(findIterable.collation(any())).thenReturn(findIterable);
@@ -845,8 +847,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@Test // GH-4277
void findShouldUseReadConcernWhenPresent() {
template.find(new BasicQuery("{'foo' : 'bar'}").withReadConcern(ReadConcern.SNAPSHOT),
AutogenerateableId.class);
template.find(new BasicQuery("{'foo' : 'bar'}").withReadConcern(ReadConcern.SNAPSHOT), AutogenerateableId.class);
verify(collection).withReadConcern(ReadConcern.SNAPSHOT);
}
@@ -1002,7 +1003,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), new Update(), AutogenerateableId.class);
ArgumentCaptor<ReplaceOptions> options = ArgumentCaptor.forClass(ReplaceOptions.class);
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(), any(), options.capture());
assertThat(options.getValue().getCollation().getLocale()).isEqualTo("fr");
@@ -1129,8 +1131,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() {
template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
PersonProjection.class,
CursorPreparer.NO_OP_PREPARER);
PersonProjection.class, CursorPreparer.NO_OP_PREPARER);
verify(findIterable).projection(eq(new Document("firstname", 1)));
}
@@ -1139,8 +1140,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() {
template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class,
PersonProjection.class,
CursorPreparer.NO_OP_PREPARER);
PersonProjection.class, CursorPreparer.NO_OP_PREPARER);
verify(findIterable).projection(eq(new Document("bar", 1)));
}
@@ -1149,8 +1149,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() {
template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
PersonSpELProjection.class,
CursorPreparer.NO_OP_PREPARER);
PersonSpELProjection.class, CursorPreparer.NO_OP_PREPARER);
verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT));
}
@@ -1159,8 +1158,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
void appliesFieldsToDtoProjection() {
template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
Jedi.class,
CursorPreparer.NO_OP_PREPARER);
Jedi.class, CursorPreparer.NO_OP_PREPARER);
verify(findIterable).projection(eq(new Document("firstname", 1)));
}
@@ -1169,8 +1167,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() {
template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class,
Jedi.class,
CursorPreparer.NO_OP_PREPARER);
Jedi.class, CursorPreparer.NO_OP_PREPARER);
verify(findIterable).projection(eq(new Document("bar", 1)));
}
@@ -1179,8 +1176,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
void doesNotApplyFieldsWhenTargetIsNotAProjection() {
template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
Person.class,
CursorPreparer.NO_OP_PREPARER);
Person.class, CursorPreparer.NO_OP_PREPARER);
verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT));
}
@@ -1189,8 +1185,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
void doesNotApplyFieldsWhenTargetExtendsDomainType() {
template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
PersonExtended.class,
CursorPreparer.NO_OP_PREPARER);
PersonExtended.class, CursorPreparer.NO_OP_PREPARER);
verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT));
}
@@ -1243,7 +1238,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
template.save(entity);
verify(collection, times(1)).replaceOne(queryCaptor.capture(), updateCaptor.capture(), any(ReplaceOptions.class));
verify(collection, times(1)).replaceOne(queryCaptor.capture(), updateCaptor.capture(), any(com.mongodb.client.model.ReplaceOptions.class));
assertThat(queryCaptor.getValue()).isEqualTo(new Document("_id", 1).append("version", 10));
assertThat(updateCaptor.getValue())
@@ -1785,7 +1780,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
ArgumentCaptor<Document> captor = ArgumentCaptor.forClass(Document.class);
verify(collection).replaceOne(any(), captor.capture(), any(ReplaceOptions.class));
verify(collection).replaceOne(any(), captor.capture(), any(com.mongodb.client.model.ReplaceOptions.class));
assertThat(captor.getValue()).containsEntry("added-by", "callback");
}
@@ -2005,7 +2000,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@Test // DATAMONGO-2341
void saveShouldAppendNonDefaultShardKeyToVersionedEntityIfNotPresentInFilter() {
when(collection.replaceOne(any(), any(), any(ReplaceOptions.class)))
when(collection.replaceOne(any(), any(), any(com.mongodb.client.model.ReplaceOptions.class)))
.thenReturn(UpdateResult.acknowledged(1, 1L, null));
template.save(new ShardedVersionedEntityWithNonDefaultShardKey("id-1", 1L, "AT", 4230));
@@ -2093,7 +2088,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@Test // DATAMONGO-2341
void saveVersionedShouldProjectOnShardKeyWhenLoadingExistingDocument() {
when(collection.replaceOne(any(), any(), any(ReplaceOptions.class)))
when(collection.replaceOne(any(), any(), any(com.mongodb.client.model.ReplaceOptions.class)))
.thenReturn(UpdateResult.acknowledged(1, 1L, null));
when(findIterable.first()).thenReturn(new Document("_id", "id-1").append("country", "US").append("userid", 4230));
@@ -2442,6 +2437,83 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
any(FindOneAndReplaceOptions.class));
}
@Test // GH-4462
void replaceShouldUseCollationWhenPresent() {
template.replace(new BasicQuery("{}").collation(Collation.of("fr")), new AutogenerateableId());
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(), any(), options.capture());
assertThat(options.getValue().isUpsert()).isFalse();
assertThat(options.getValue().getCollation().getLocale()).isEqualTo("fr");
}
@Test // GH-4462
void replaceShouldNotUpsertByDefault() {
template.replace(new BasicQuery("{}"), new Sith());
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(), any(), options.capture());
assertThat(options.getValue().isUpsert()).isFalse();
}
@Test // GH-4462
void replaceShouldUpsert() {
template.replace(new BasicQuery("{}"), new Sith(), ReplaceOptions.replaceOptions().upsert());
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(), any(), options.capture());
assertThat(options.getValue().isUpsert()).isTrue();
}
@Test // GH-4462
void replaceShouldUseDefaultCollationWhenPresent() {
template.replace(new BasicQuery("{}"), new Sith(), ReplaceOptions.replaceOptions());
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(), any(), options.capture());
assertThat(options.getValue().getCollation().getLocale()).isEqualTo("de_AT");
}
@Test // GH-4462
void replaceShouldUseHintIfPresent() {
template.replace(new BasicQuery("{}").withHint("index-to-use"), new Sith(), ReplaceOptions.replaceOptions().upsert());
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(), any(), options.capture());
assertThat(options.getValue().getHintString()).isEqualTo("index-to-use");
}
@Test // GH-4462
void replaceShouldApplyWriteConcern() {
template.setWriteConcernResolver(new WriteConcernResolver() {
public WriteConcern resolve(MongoAction action) {
assertThat(action.getMongoActionOperation()).isEqualTo(MongoActionOperation.REPLACE);
return WriteConcern.UNACKNOWLEDGED;
}
});
template.replace(new BasicQuery("{}").withHint("index-to-use"), new Sith(), ReplaceOptions.replaceOptions().upsert());
verify(collection).withWriteConcern(eq(WriteConcern.UNACKNOWLEDGED));
}
class AutogenerateableId {
@Id BigInteger id;

View File

@@ -0,0 +1,309 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.ReplaceOptions.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bson.BsonInt64;
import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.reactivestreams.Publisher;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension;
import com.mongodb.client.model.Filters;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoCollection;
/**
* @author Christoph Strobl
*/
@ExtendWith(MongoClientExtension.class)
public class ReactiveMongoTemplateReplaceTests {
static final String DB_NAME = "mongo-template-replace-tests";
static final String RESTAURANT_COLLECTION = "restaurant";
static @Client MongoClient client;
private ReactiveMongoTemplate template;
@BeforeEach
void beforeEach() {
template = new ReactiveMongoTemplate(client, DB_NAME);
template.setEntityLifecycleEventsEnabled(false);
initTestData();
}
@AfterEach()
void afterEach() {
clearTestData();
}
@Test // GH-4462
void replacesExistingDocument() {
Mono<UpdateResult> result = template.replace(query(where("name").is("Central Perk Cafe")),
new Restaurant("Central Pork Cafe", "Manhattan"));
result.as(StepVerifier::create).consumeNextWith(it -> {
assertThat(it.getMatchedCount()).isEqualTo(1);
assertThat(it.getModifiedCount()).isEqualTo(1);
}).verifyComplete();
retrieve(collection -> collection.find(Filters.eq("_id", 1)).first()).as(StepVerifier::create)
.consumeNextWith(document -> {
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}).verifyComplete();
}
@Test // GH-4462
void replacesFirstOnMoreThanOneMatch() {
Mono<UpdateResult> result = template.replace(query(where("violations").exists(true)),
new Restaurant("Central Pork Cafe", "Manhattan"));
result.as(StepVerifier::create).consumeNextWith(it -> {
assertThat(it.getMatchedCount()).isEqualTo(1);
assertThat(it.getModifiedCount()).isEqualTo(1);
}).verifyComplete();
retrieve(collection -> collection.find(Filters.eq("_id", 2)).first()).as(StepVerifier::create)
.consumeNextWith(document -> {
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}).verifyComplete();
}
@Test // GH-4462
void replacesExistingDocumentWithRawDoc() {
Mono<UpdateResult> result = template.replace(query(where("r-name").is("Central Perk Cafe")),
Document.parse("{ 'r-name' : 'Central Pork Cafe', 'Borough' : 'Manhattan' }"),
template.getCollectionName(Restaurant.class));
result.as(StepVerifier::create).consumeNextWith(it -> {
assertThat(it.getMatchedCount()).isEqualTo(1);
assertThat(it.getModifiedCount()).isEqualTo(1);
}).verifyComplete();
retrieve(collection -> collection.find(Filters.eq("_id", 1)).first()).as(StepVerifier::create)
.consumeNextWith(document -> {
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}).verifyComplete();
}
@Test // GH-4462
void replacesExistingDocumentWithRawDocMappingQueryAgainstDomainType() {
Mono<UpdateResult> result = template.replace(query(where("name").is("Central Perk Cafe")), Restaurant.class,
Document.parse("{ 'r-name' : 'Central Pork Cafe', 'Borough' : 'Manhattan' }"), ReplaceOptions.none(), template.getCollectionName(Restaurant.class));
result.as(StepVerifier::create).consumeNextWith(it -> {
assertThat(it.getMatchedCount()).isEqualTo(1);
assertThat(it.getModifiedCount()).isEqualTo(1);
}).verifyComplete();
retrieve(collection -> collection.find(Filters.eq("_id", 1)).first()).as(StepVerifier::create)
.consumeNextWith(document -> {
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}).verifyComplete();
}
@Test // GH-4462
void replacesExistingDocumentWithMatchingId() {
Mono<UpdateResult> result = template.replace(query(where("name").is("Central Perk Cafe")),
new Restaurant(1L, "Central Pork Cafe", "Manhattan", 0));
result.as(StepVerifier::create).consumeNextWith(it -> {
assertThat(it.getMatchedCount()).isEqualTo(1);
assertThat(it.getModifiedCount()).isEqualTo(1);
}).verifyComplete();
retrieve(collection -> collection.find(Filters.eq("_id", 1)).first()).as(StepVerifier::create)
.consumeNextWith(document -> {
assertThat(document).containsEntry("r-name", "Central Pork Cafe");
}).verifyComplete();
}
@Test // GH-4462
void replacesExistingDocumentWithNewIdThrowsDataIntegrityViolationException() {
template.replace(query(where("name").is("Central Perk Cafe")),
new Restaurant(4L, "Central Pork Cafe", "Manhattan", 0))
.as(StepVerifier::create)
.expectError(DataIntegrityViolationException.class)
.verify();
}
@Test // GH-4462
void doesNothingIfNoMatchFoundAndUpsertSetToFalse/* by default */() {
Mono<UpdateResult> result = template.replace(query(where("name").is("Pizza Rat's Pizzaria")),
new Restaurant(null, "Pizza Rat's Pizzaria", "Manhattan", 8));
result.as(StepVerifier::create).consumeNextWith(it -> {
assertThat(it.getMatchedCount()).isEqualTo(0);
assertThat(it.getModifiedCount()).isEqualTo(0);
}).verifyComplete();
retrieve(collection -> collection.find(Filters.eq("r-name", "Pizza Rat's Pizzaria")).first())
.as(StepVerifier::create).verifyComplete();
}
@Test // GH-4462
void insertsIfNoMatchFoundAndUpsertSetToTrue() {
Mono<UpdateResult> result = template.replace(query(where("name").is("Pizza Rat's Pizzaria")),
new Restaurant(4L, "Pizza Rat's Pizzaria", "Manhattan", 8), replaceOptions().upsert());
result.as(StepVerifier::create).consumeNextWith(it -> {
assertThat(it.getMatchedCount()).isEqualTo(0);
assertThat(it.getModifiedCount()).isEqualTo(0);
assertThat(it.getUpsertedId()).isEqualTo(new BsonInt64(4L));
}).verifyComplete();
retrieve(collection -> collection.find(Filters.eq("_id", 4)).first()).as(StepVerifier::create)
.consumeNextWith(document -> {
assertThat(document).containsEntry("r-name", "Pizza Rat's Pizzaria");
});
}
void initTestData() {
List<Document> testData = Stream.of( //
"{ '_id' : 1, 'r-name' : 'Central Perk Cafe', 'Borough' : 'Manhattan' }",
"{ '_id' : 2, 'r-name' : 'Rock A Feller Bar and Grill', 'Borough' : 'Queens', 'violations' : 2 }",
"{ '_id' : 3, 'r-name' : 'Empire State Pub', 'Borough' : 'Brooklyn', 'violations' : 0 }") //
.map(Document::parse).collect(Collectors.toList());
doInCollection(collection -> collection.insertMany(testData));
}
void clearTestData() {
doInCollection(collection -> collection.deleteMany(new Document()));
}
void doInCollection(Function<MongoCollection<Document>, Publisher<?>> fkt) {
retrieve(collection -> Mono.from(fkt.apply(collection))).then().as(StepVerifier::create).verifyComplete();
}
<T> Mono<T> retrieve(Function<MongoCollection<Document>, Publisher<T>> fkt) {
return Mono.from(fkt.apply(client.getDatabase(DB_NAME).getCollection(RESTAURANT_COLLECTION)));
}
@org.springframework.data.mongodb.core.mapping.Document(RESTAURANT_COLLECTION)
static class Restaurant {
Long id;
@Field("r-name") String name;
String borough;
Integer violations;
Restaurant() {}
Restaurant(String name, String borough) {
this.name = name;
this.borough = borough;
}
Restaurant(Long id, String name, String borough, Integer violations) {
this.id = id;
this.name = name;
this.borough = borough;
this.violations = violations;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRName() {
return name;
}
public void setRName(String rName) {
this.name = rName;
}
public String getBorough() {
return borough;
}
public void setBorough(String borough) {
this.borough = borough;
}
public int getViolations() {
return violations;
}
public void setViolations(int violations) {
this.violations = violations;
}
@Override
public String toString() {
return "Restaurant{" + "id=" + id + ", name='" + name + '\'' + ", borough='" + borough + '\'' + ", violations="
+ violations + '}';
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Restaurant that = (Restaurant) o;
return violations == that.violations && Objects.equals(id, that.id) && Objects.equals(name, that.name)
&& Objects.equals(borough, that.borough);
}
@Override
public int hashCode() {
return Objects.hash(id, name, borough, violations);
}
}
}

View File

@@ -20,6 +20,8 @@ import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
import com.mongodb.WriteConcern;
import org.springframework.data.mongodb.core.MongoTemplateUnitTests.Sith;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -1602,6 +1604,83 @@ public class ReactiveMongoTemplateUnitTests {
verify(changeStreamPublisher).startAfter(eq(token));
}
@Test // GH-4462
void replaceShouldUseCollationWhenPresent() {
template.replace(new BasicQuery("{}").collation(Collation.of("fr")), new Jedi()).subscribe();
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(Bson.class), any(), options.capture());
assertThat(options.getValue().isUpsert()).isFalse();
assertThat(options.getValue().getCollation().getLocale()).isEqualTo("fr");
}
@Test // GH-4462
void replaceShouldNotUpsertByDefault() {
template.replace(new BasicQuery("{}"), new MongoTemplateUnitTests.Sith()).subscribe();
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(Bson.class), any(), options.capture());
assertThat(options.getValue().isUpsert()).isFalse();
}
@Test // GH-4462
void replaceShouldUpsert() {
template.replace(new BasicQuery("{}"), new MongoTemplateUnitTests.Sith(), org.springframework.data.mongodb.core.ReplaceOptions.replaceOptions().upsert()).subscribe();
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(Bson.class), any(), options.capture());
assertThat(options.getValue().isUpsert()).isTrue();
}
@Test // GH-4462
void replaceShouldUseDefaultCollationWhenPresent() {
template.replace(new BasicQuery("{}"), new MongoTemplateUnitTests.Sith(), org.springframework.data.mongodb.core.ReplaceOptions.replaceOptions()).subscribe();
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(Bson.class), any(), options.capture());
assertThat(options.getValue().getCollation().getLocale()).isEqualTo("de_AT");
}
@Test // GH-4462
void replaceShouldUseHintIfPresent() {
template.replace(new BasicQuery("{}").withHint("index-to-use"), new MongoTemplateUnitTests.Sith(), org.springframework.data.mongodb.core.ReplaceOptions.replaceOptions().upsert()).subscribe();
ArgumentCaptor<com.mongodb.client.model.ReplaceOptions> options = ArgumentCaptor
.forClass(com.mongodb.client.model.ReplaceOptions.class);
verify(collection).replaceOne(any(Bson.class), any(), options.capture());
assertThat(options.getValue().getHintString()).isEqualTo("index-to-use");
}
@Test // GH-4462
void replaceShouldApplyWriteConcern() {
template.setWriteConcernResolver(new WriteConcernResolver() {
public WriteConcern resolve(MongoAction action) {
assertThat(action.getMongoActionOperation()).isEqualTo(MongoActionOperation.REPLACE);
return WriteConcern.UNACKNOWLEDGED;
}
});
template.replace(new BasicQuery("{}").withHint("index-to-use"), new Sith(), org.springframework.data.mongodb.core.ReplaceOptions.replaceOptions().upsert()).subscribe();
verify(collection).withWriteConcern(eq(WriteConcern.UNACKNOWLEDGED));
}
private void stubFindSubscribe(Document document) {
Publisher<Document> realPublisher = Flux.just(document);

View File

@@ -670,7 +670,7 @@ The query syntax used in the preceding example is explained in more detail in th
[[mongo-template.id-handling]]
=== How the `_id` Field is Handled in the Mapping Layer
MongoDB requires that you have an `_id` field for all documents. If you do not provide one, the driver assigns an `ObjectId` with a generated value. When you use the `MappingMongoConverter`, certain rules govern how properties from the Java class are mapped to this `_id` field:
MongoDB requires that you have an `_id` field for all documents. If you do not provide one, the driver assigns an `ObjectId` with a generated value without considering your domain model as the server isn't aware of your identifier type. When you use the `MappingMongoConverter`, certain rules govern how properties from the Java class are mapped to this `_id` field:
. A property or field annotated with `@Id` (`org.springframework.data.annotation.Id`) maps to the `_id` field.
. A property or field without an annotation but named `id` maps to the `_id` field.
@@ -980,6 +980,55 @@ template.update(Person.class)
WARNING: `upsert` does not support ordering. Please use <<mongo-template.find-and-upsert, findAndModify>> to apply `Sort`.
[[mongo-template.replace]]
=== Replacing Documents in a Collection
The various `replace` methods available via `MongoTemplate` allow to override the first matching Document.
If no match is found a new one can be upserted (as outlined in the previous section) by providing `ReplaceOptions` with according configuration.
====
.Replace one
[source,java]
----
Person tom = template.insert(new Person("Motte", 21)); <1>
Query query = Query.query(Criteria.where("firstName").is(tom.getFirstName())); <2>
tom.setFirstname("Tom"); <3>
template.replace(query, tom, ReplaceOptions.none()); <4>
----
<1> Insert a new document.
<2> The query used to identify the single document to replace.
<3> Set up the replacement document which must hold either the same `_id` as the existing or no `_id` at all.
<4> Run the replace operation.
.Replace one with upsert
[source,java]
----
Person tom = new Person("id-123", "Tom", 21) <1>
Query query = Query.query(Criteria.where("firstName").is(tom.getFirstName()));
template.replace(query, tom, ReplaceOptions.replaceOptions().upsert()); <2>
----
<1> The `_id` value needs to be provided for upsert, otherwise MongoDB will generate an `ObjectId`.
As MongoDB is not aware of your domain type, any `@Field(targetType)` hints are not considered and the resulting `ObjectId` might be not compatible with your domain model.
<2> Use `upsert` to insert a new document if no match is found.
====
[WARNING]
====
It is not possible to change the `_id` of existing documents with a replace operation.
On `upsert` MongoDB uses 2 ways of determining the new id for the entry:
* The `_id` is used within the query as in `{"_id" : 1234 }`
* The `_id` is present in the replacement document.
If no `_id` is provided in either way, MongoDB will create a new `ObjectId` for the document.
This may lead to mapping and data lookup malfunctions if the used domain types `id` property has a different type like e. g. `Long`.
====
[[mongo-template.find-and-upsert]]
=== Finding and Upserting Documents in a Collection