diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 2f3c0dd926..c1d6545df7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -1753,6 +1753,129 @@ default long exactCount(Query query, String collectionName) { */ List findAllAndRemove(Query query, Class entityClass, String collectionName); + /** + * Triggers + * replaceOne + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} + * document.
+ * The collection name is derived from the {@literal replacement} type.
+ * Options are defaulted to {@link ReplaceOptions#empty()}.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. Must not be {@literal null}. + * @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. + */ + default UpdateResult replace(Query query, T replacement) { + return replace(query, replacement, ReplaceOptions.empty()); + } + + /** + * Triggers + * replaceOne + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} + * document.
+ * Options are defaulted to {@link ReplaceOptions#empty()}.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. Must not be {@literal null}. + * @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. + * @throws org.springframework.data.mapping.MappingException if the collection name cannot be + * {@link #getCollectionName(Class) derived} from the given replacement value. + */ + default UpdateResult replace(Query query, T replacement, String collectionName) { + return replace(query, replacement, ReplaceOptions.empty(), collectionName); + } + + /** + * Triggers + * replaceOne + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document + * taking {@link ReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. Must not be {@literal null}. + * @param replacement the replacement document. Must not be {@literal null}. + * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}. + * @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. + */ + default UpdateResult replace(Query query, T replacement, ReplaceOptions options) { + return replace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement))); + } + + /** + * Triggers + * replaceOne + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document + * taking {@link ReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. Must not be {@literal null}. + * @param replacement the replacement document. Must not be {@literal null}. + * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}. + * @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. + */ + default UpdateResult replace(Query query, T replacement, ReplaceOptions options, String collectionName) { + + Assert.notNull(replacement, "Replacement must not be null"); + return replace(query, replacement, options, (Class) ClassUtils.getUserClass(replacement), collectionName); + } + + /** + * Triggers + * replaceOne + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document + * taking {@link ReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. Must not be {@literal null}. + * @param replacement the replacement document. Must not be {@literal null}. + * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}. + * @param entityType the type used for mapping the {@link Query} to domain type fields and deriving the collection + * from. Must not be {@literal null}. + * @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. + */ + default UpdateResult replace(Query query, S replacement, ReplaceOptions options, Class entityType) { + + return replace(query, replacement, options, entityType, + getCollectionName(ClassUtils.getUserClass(entityType))); + } + + /** + * Triggers + * replaceOne + * to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document + * taking {@link ReplaceOptions} into account.
+ * NOTE: The replacement entity must not hold an {@literal id}. + * + * @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional + * fields specification. Must not be {@literal null}. + * @param replacement the replacement document. Must not be {@literal null}. + * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}. + * @param entityType the type used for mapping the {@link Query} to domain type fields. Must not be {@literal null}. + * @param collectionName the collection to query. Must not be {@literal null}. + * @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. + */ + UpdateResult replace(Query query, S replacement, ReplaceOptions options, Class entityType, + String collectionName); + /** * Returns the underlying {@link MongoConverter}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 3190cf0ac3..b2d0d04615 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -178,6 +178,7 @@ * @author Anton Barkan * @author Bartłomiej Mazur * @author Michael Krog + * @author Jakub Zurawa */ public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider, ReadPreferenceAware { @@ -1618,7 +1619,7 @@ protected Object saveDocument(String collectionName, Document dbDoc, Class en } } - 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 @@ protected UpdateResult doUpdate(String collectionName, Query query, UpdateDefini } } - 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) @@ -3421,6 +3422,56 @@ public MongoDatabaseFactory getMongoDatabaseFactory() { return mongoDbFactory; } + @Override + public UpdateResult replace(Query query, S replacement, ReplaceOptions options, Class entityType, + 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#empty() 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"); + + MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityType); + QueryContext queryContext = queryOperations.createQueryContext(query); + + CollectionPreparerDelegate collectionPreparer = createDelegate(query); + Document mappedQuery = queryContext.getMappedQuery(entity); + + replacement = maybeCallBeforeConvert(replacement, collectionName); + Document mappedReplacement = operations.forEntity(replacement).toMappedDocument(this.mongoConverter).getDocument(); + maybeCallBeforeSave(replacement, mappedReplacement, collectionName); + + maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName)); + maybeCallBeforeSave(replacement, mappedReplacement, collectionName); + + UpdateResult result = doReplace(options, entityType, collectionName, queryContext, collectionPreparer, mappedQuery, + mappedReplacement); + + if (result.wasAcknowledged()) { + maybeEmitEvent(new AfterSaveEvent<>(replacement, mappedReplacement, collectionName)); + maybeCallAfterSave(replacement, mappedReplacement, collectionName); + } + + return result; + } + + private UpdateResult doReplace(ReplaceOptions options, Class entityType, String collectionName, + QueryContext queryContext, CollectionPreparerDelegate collectionPreparer, Document mappedQuery, + Document replacement) { + ReplaceCallback replaceCallback = new ReplaceCallback(collectionPreparer, mappedQuery, replacement, + queryContext.getCollation(entityType).orElse(null), options); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + String.format("findAndReplace using query: %s for class: %s and replacement: %s " + "in collection: %s", + serializeToJsonSafely(mappedQuery), entityType, serializeToJsonSafely(replacement), collectionName)); + } + + return execute(collectionName, replaceCallback); + } + /** * A {@link CloseableIterator} that is backed by a MongoDB {@link MongoCollection}. * @@ -3555,4 +3606,33 @@ interface CountExecution { long countDocuments(CollectionPreparer collectionPreparer, String collection, Document filter, CountOptions options); } + + private static class ReplaceCallback implements CollectionCallback { + + private final CollectionPreparer> collectionPreparer; + private final Document query; + private final Document update; + private final @Nullable com.mongodb.client.model.Collation collation; + private final ReplaceOptions options; + + ReplaceCallback(CollectionPreparer> collectionPreparer, Document query, Document update, + @Nullable com.mongodb.client.model.Collation collation, ReplaceOptions options) { + this.collectionPreparer = collectionPreparer; + this.query = query; + this.update = update; + this.options = options; + this.collation = collation; + } + + @Override + public UpdateResult doInCollection(MongoCollection collection) + throws MongoException, DataAccessException { + com.mongodb.client.model.ReplaceOptions opts = new com.mongodb.client.model.ReplaceOptions(); + opts.collation(collation); + + opts.upsert(options.isUpsert()); + + return collectionPreparer.prepare(collection).replaceOne(query, update, opts); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java new file mode 100644 index 0000000000..f5a0d0eabe --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java @@ -0,0 +1,84 @@ +/** + * Options for + * replaceOne. + *
+ * Defaults to + *
+ *
upsert
+ *
false
+ *
+ * + * @author Jakub Zurawa + */ +package org.springframework.data.mongodb.core; + +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. + *
+ *
upsert
+ *
false
+ *
+ * + * @return new instance of {@link ReplaceOptions}. + */ + public static ReplaceOptions options() { + return new ReplaceOptions(); + } + + /** + * Static factory method returning an unmodifiable {@link ReplaceOptions} instance. + * + * @return unmodifiable {@link ReplaceOptions} instance. + * @since 2.2 + */ + public static ReplaceOptions none() { + return NONE; + } + + /** + * Static factory method to create a {@link ReplaceOptions} instance with + *
+ *
upsert
+ *
false
+ *
+ * + * @return new instance of {@link ReplaceOptions}. + */ + public static ReplaceOptions empty() { + return new ReplaceOptions(); + } + + /** + * 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; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 0a962db448..e2b3dc80d3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -116,6 +116,7 @@ * @author Mark Paluch * @author Laszlo Csontos * @author duozhilin + * @author Jakub Zurawa */ @ExtendWith(MongoClientExtension.class) public class MongoTemplateTests { @@ -3872,6 +3873,21 @@ void shouldExecuteQueryWithExpression() { 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.options(), + collectionName); + + assertThat(updateResult.wasAcknowledged()).isTrue(); + assertThat(template.findOne(query(where("foo").is("baz")), org.bson.Document.class, collectionName)).isNotNull(); + } + private AtomicReference createAfterSaveReference() { AtomicReference saved = new AtomicReference<>(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index d770e960e1..44b8f6642a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -131,6 +131,7 @@ * @author Michael J. Simons * @author Roman Puchkovskiy * @author Yadhukrishna S Pai + * @author Jakub Zurawa */ @MockitoSettings(strictness = Strictness.LENIENT) public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @@ -173,7 +174,7 @@ void beforeEach() { 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); @@ -845,8 +846,7 @@ void executeQueryShouldUseBatchSizeWhenPresent() { @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 +1002,8 @@ void replaceOneShouldUseCollationWhenPresent() { template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), new Update(), AutogenerateableId.class); - ArgumentCaptor options = ArgumentCaptor.forClass(ReplaceOptions.class); + ArgumentCaptor 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 +1130,7 @@ void countShouldApplyQueryHintAsIndexNameIfPresent() { 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 +1139,7 @@ void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { 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 +1148,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { 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 +1157,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { 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 +1166,7 @@ void appliesFieldsToDtoProjection() { 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 +1175,7 @@ void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { 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 +1184,7 @@ void doesNotApplyFieldsWhenTargetIsNotAProjection() { 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 +1237,7 @@ void saveVersionedEntityShouldCallUpdateCorrectly() { 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 +1779,7 @@ public Person onBeforeSave(Person entity, Document document, String collection) ArgumentCaptor 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 +1999,7 @@ void saveShouldAppendNonDefaultShardKeyIfNotPresentInFilter() { @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 +2087,7 @@ void saveShouldProjectOnShardKeyWhenLoadingExistingDocument() { @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 +2436,44 @@ void findAndReplaceAllowsDocumentSourceType() { any(FindOneAndReplaceOptions.class)); } + @Test // GH-4300 + void replaceShouldUseCollationWhenPresent() { + + template.replace(new BasicQuery("{}").collation(Collation.of("fr")), new AutogenerateableId()); + + ArgumentCaptor 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-4300 + void replaceShouldUpsert() { + + template.replace(new BasicQuery("{}"), new Sith(), ReplaceOptions.options().upsert()); + + ArgumentCaptor options = ArgumentCaptor + .forClass(com.mongodb.client.model.ReplaceOptions.class); + verify(collection).replaceOne(any(), any(), options.capture()); + + assertThat(options.getValue().isUpsert()).isTrue(); + + } + + @Test // GH-4300 + void replaceShouldUseDefaultCollationWhenPresent() { + + template.replace(new BasicQuery("{}"), new Sith(), ReplaceOptions.options()); + + ArgumentCaptor options = ArgumentCaptor + .forClass(com.mongodb.client.model.ReplaceOptions.class); + verify(collection).replaceOne(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation().getLocale()).isEqualTo("de_AT"); + } + class AutogenerateableId { @Id BigInteger id;