diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperations.java index 5ec0f25aa..4820c2355 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperations.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core; import java.util.List; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -81,7 +82,8 @@ public interface BulkOperations { /** * Add a single update to the bulk operation. For the update request, only the first matching document is updated. * - * @param query update criteria, must not be {@literal null}. + * @param query update criteria, must not be {@literal null}. The {@link Query} may define a {@link Query#with(Sort) + * sort order} to influence which document to update when potentially matching multiple candidates. * @param update {@link Update} operation to perform, must not be {@literal null}. * @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}. */ @@ -92,7 +94,8 @@ public interface BulkOperations { /** * Add a single update to the bulk operation. For the update request, only the first matching document is updated. * - * @param query update criteria, must not be {@literal null}. + * @param query update criteria, must not be {@literal null}. The {@link Query} may define a {@link Query#with(Sort) + * sort order} to influence which document to update when potentially matching multiple candidates. * @param update {@link Update} operation to perform, must not be {@literal null}. * @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}. * @since 4.1 @@ -187,7 +190,8 @@ public interface BulkOperations { /** * Add a single replace operation to the bulk operation. * - * @param query Update criteria. + * @param query Replace criteria. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence + * which document to replace when potentially matching multiple candidates. * @param replacement the replacement document. Must not be {@literal null}. * @return the current {@link BulkOperations} instance with the replacement added, will never be {@literal null}. * @since 2.2 @@ -199,7 +203,8 @@ public interface BulkOperations { /** * Add a single replace operation to the bulk operation. * - * @param query Update criteria. + * @param query Replace criteria. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence + * which document to replace when potentially matching multiple candidates. * @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 current {@link BulkOperations} instance with the replacement added, will never be {@literal null}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperationsSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperationsSupport.java index 8ca0a09b2..1f5509cd6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperationsSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperationsSupport.java @@ -106,6 +106,11 @@ abstract class BulkOperationsSupport { if (writeModel instanceof UpdateOneModel model) { + Bson sort = model.getOptions().getSort(); + if (sort instanceof Document sortDocument) { + model.getOptions().sort(updateMapper().getMappedSort(sortDocument, entity().orElse(null))); + } + if (source instanceof AggregationUpdate aggregationUpdate) { List pipeline = mapUpdatePipeline(aggregationUpdate); @@ -136,6 +141,17 @@ abstract class BulkOperationsSupport { return new DeleteManyModel<>(getMappedQuery(model.getFilter()), model.getOptions()); } + if (writeModel instanceof ReplaceOneModel model) { + + Bson sort = model.getReplaceOptions().getSort(); + + if (sort instanceof Document sortDocument) { + model.getReplaceOptions().sort(updateMapper().getMappedSort(sortDocument, entity().orElse(null))); + } + return new ReplaceOneModel<>(getMappedQuery(model.getFilter()), model.getReplacement(), + model.getReplaceOptions()); + } + return writeModel; } @@ -192,9 +208,11 @@ abstract class BulkOperationsSupport { * @param filterQuery The {@link Query} to read a potential {@link Collation} from. Must not be {@literal null}. * @param update The {@link Update} to apply * @param upsert flag to indicate if document should be upserted. + * @param multi flag to indicate if update might affect multiple documents. * @return new instance of {@link UpdateOptions}. */ - protected static UpdateOptions computeUpdateOptions(Query filterQuery, UpdateDefinition update, boolean upsert) { + protected UpdateOptions computeUpdateOptions(Query filterQuery, UpdateDefinition update, boolean upsert, + boolean multi) { UpdateOptions options = new UpdateOptions(); options.upsert(upsert); @@ -207,6 +225,10 @@ abstract class BulkOperationsSupport { options.arrayFilters(list); } + if (!multi && filterQuery.isSorted()) { + options.sort(filterQuery.getSortObject()); + } + filterQuery.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation); return options; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java index 80b2cfe33..52343522a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java @@ -229,12 +229,14 @@ class DefaultBulkOperations extends BulkOperationsSupport implements BulkOperati ReplaceOptions replaceOptions = new ReplaceOptions(); replaceOptions.upsert(options.isUpsert()); + if (query.isSorted()) { + replaceOptions.sort(query.getSortObject()); + } query.getCollation().map(Collation::toMongoCollation).ifPresent(replaceOptions::collation); maybeEmitEvent(new BeforeConvertEvent<>(replacement, collectionName)); Object source = maybeInvokeBeforeConvertCallback(replacement); - addModel(source, - new ReplaceOneModel<>(getMappedQuery(query.getQueryObject()), getMappedObject(source), replaceOptions)); + addModel(source, new ReplaceOneModel<>(query.getQueryObject(), getMappedObject(source), replaceOptions)); return this; } @@ -315,7 +317,7 @@ class DefaultBulkOperations extends BulkOperationsSupport implements BulkOperati Assert.notNull(query, "Query must not be null"); Assert.notNull(update, "Update must not be null"); - UpdateOptions options = computeUpdateOptions(query, update, upsert); + UpdateOptions options = computeUpdateOptions(query, update, upsert, multi); if (multi) { addModel(update, new UpdateManyModel<>(query.getQueryObject(), update.getUpdateObject(), options)); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java index bf9dea69c..59b7ccd63 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java @@ -189,13 +189,16 @@ class DefaultReactiveBulkOperations extends BulkOperationsSupport implements Rea ReplaceOptions replaceOptions = new ReplaceOptions(); replaceOptions.upsert(options.isUpsert()); + if (query.isSorted()) { + replaceOptions.sort(query.getSortObject()); + } query.getCollation().map(Collation::toMongoCollation).ifPresent(replaceOptions::collation); this.models.add(Mono.just(replacement).flatMap(it -> { maybeEmitEvent(new BeforeConvertEvent<>(it, collectionName)); return maybeInvokeBeforeConvertCallback(it); }).map(it -> new SourceAwareWriteModelHolder(it, - new ReplaceOneModel<>(getMappedQuery(query.getQueryObject()), getMappedObject(it), replaceOptions)))); + new ReplaceOneModel<>(query.getQueryObject(), getMappedObject(it), replaceOptions)))); return this; } @@ -218,13 +221,13 @@ class DefaultReactiveBulkOperations extends BulkOperationsSupport implements Rea Flux concat = Flux.concat(models).flatMapSequential(it -> { - if (it.model()instanceof InsertOneModel iom) { + if (it.model() instanceof InsertOneModel iom) { Document target = iom.getDocument(); maybeEmitBeforeSaveEvent(it); return maybeInvokeBeforeSaveCallback(it.source(), target) .map(afterCallback -> new SourceAwareWriteModelHolder(afterCallback, mapWriteModel(afterCallback, iom))); - } else if (it.model()instanceof ReplaceOneModel rom) { + } else if (it.model() instanceof ReplaceOneModel rom) { Document target = rom.getReplacement(); maybeEmitBeforeSaveEvent(it); @@ -265,7 +268,7 @@ class DefaultReactiveBulkOperations extends BulkOperationsSupport implements Rea Assert.notNull(query, "Query must not be null"); Assert.notNull(update, "Update must not be null"); - UpdateOptions options = computeUpdateOptions(query, update, upsert); + UpdateOptions options = computeUpdateOptions(query, update, upsert, multi); this.models.add(Mono.just(update).map(it -> { if (multi) { 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 ba24983c4..f400b35a6 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 @@ -25,6 +25,7 @@ import java.util.stream.Stream; import org.bson.Document; import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; import org.springframework.data.geo.GeoResults; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; @@ -1604,8 +1605,9 @@ public interface MongoOperations extends FluentMongoOperations { * A potential {@link org.springframework.data.annotation.Version} property of the {@literal entityClass} will be * auto-incremented if not explicitly specified in the update. * - * @param query the query document that specifies the criteria used to select a document to be updated. Must not be - * {@literal null}. + * @param query the query document that specifies the criteria used to select a document to be updated. The + * {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to update when + * potentially matching multiple candidates. 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}. * @param entityClass class that determines the collection to use. @@ -1623,12 +1625,11 @@ public interface MongoOperations extends FluentMongoOperations { * the provided updated document.
* NOTE: Any additional support for field mapping, versions, etc. is not available due to the lack of * domain type information. Use {@link #updateFirst(Query, UpdateDefinition, Class, String)} to get full type specific - * support.
- * NOTE: {@link Query#getSortObject() sorting} is not supported by {@code db.collection.updateOne}. - * Use {@link #findAndModify(Query, UpdateDefinition, Class, String)} instead. + * support. * - * @param query the query document that specifies the criteria used to select a document to be updated. Must not be - * {@literal null}. + * @param query the query document that specifies the criteria used to select a document to be updated. The + * {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to update when + * potentially matching multiple candidates. 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}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. @@ -1646,8 +1647,9 @@ public interface MongoOperations extends FluentMongoOperations { * A potential {@link org.springframework.data.annotation.Version} property of the {@literal entityClass} will be auto * incremented if not explicitly specified in the update. * - * @param query the query document that specifies the criteria used to select a document to be updated. Must not be - * {@literal null}. + * @param query the query document that specifies the criteria used to select a document to be updated. The + * {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to update when + * potentially matching multiple candidates. 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}. * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. @@ -1833,7 +1835,8 @@ public interface MongoOperations extends FluentMongoOperations { * * @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}. + * to use. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to + * replace when potentially matching multiple candidates. 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 @@ -1850,7 +1853,8 @@ public interface MongoOperations extends FluentMongoOperations { * * @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}. + * to use. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to + * replace when potentially matching multiple candidates. 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. @@ -1866,7 +1870,8 @@ public interface MongoOperations extends FluentMongoOperations { * * @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}. + * to use. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to + * replace when potentially matching multiple candidates. 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. @@ -1884,7 +1889,8 @@ public interface MongoOperations extends FluentMongoOperations { * * @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}. + * to use. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to + * replace when potentially matching multiple candidates. 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. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java index b5bcf909e..28ca85fbd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java @@ -765,7 +765,7 @@ class QueryOperations { } if (query != null && query.isSorted()) { - options.sort(query.getSortObject()); + options.sort(getMappedSort(domainType != null ? mappingContext.getPersistentEntity(domainType) : null)); } HintFunction.from(getQuery().getHint()).ifPresent(codecRegistryProvider, options::hintString, options::hint); @@ -799,6 +799,9 @@ class QueryOperations { options.collation(updateOptions.getCollation()); options.upsert(updateOptions.isUpsert()); applyHint(options::hintString, options::hint); + if (!isMulti() && getQuery().isSorted()) { + options.sort(getMappedSort(domainType != null ? mappingContext.getPersistentEntity(domainType) : null)); + } if (callback != null) { callback.accept(options); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveBulkOperations.java index 5456b2e88..7f88b63f2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveBulkOperations.java @@ -19,6 +19,7 @@ import reactor.core.publisher.Mono; import java.util.List; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -67,7 +68,8 @@ public interface ReactiveBulkOperations { /** * Add a single update to the bulk operation. For the update request, only the first matching document is updated. * - * @param query update criteria, must not be {@literal null}. + * @param query update criteria, must not be {@literal null}. The {@link Query} may define a {@link Query#with(Sort) + * sort order} to influence which document to update when potentially matching multiple candidates. * @param update {@link UpdateDefinition} operation to perform, must not be {@literal null}. * @return the current {@link ReactiveBulkOperations} instance with the update added, will never be {@literal null}. */ @@ -111,8 +113,11 @@ public interface ReactiveBulkOperations { /** * Add a single replace operation to the bulk operation. * - * @param query Update criteria. - * @param replacement the replacement document. Must not be {@literal null}. + * @param query Replace criteria. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence + * which document to replace when potentially matching multiple candidates. + * @param replacement the replacement document. Must not be {@literal null}. The {@link Query} may define a + * {@link Query#with(Sort) sort order} to influence which document to replace when potentially matching + * multiple candidates. * @return the current {@link ReactiveBulkOperations} instance with the replace added, will never be {@literal null}. */ default ReactiveBulkOperations replaceOne(Query query, Object replacement) { @@ -122,7 +127,8 @@ public interface ReactiveBulkOperations { /** * Add a single replace operation to the bulk operation. * - * @param query Update criteria. + * @param query Replace criteria. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence + * which document to replace when potentially matching multiple candidates. * @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 current {@link ReactiveBulkOperations} instance with the replace added, will never be {@literal null}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java index 97ad4e08e..90f2d2345 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java @@ -26,6 +26,7 @@ import org.bson.Document; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; import org.springframework.data.geo.GeoResult; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; @@ -1502,12 +1503,11 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * the provided update document. *

* A potential {@link org.springframework.data.annotation.Version} property of the {@literal entityClass} will be - * auto-incremented if not explicitly specified in the update. NOTE: {@link Query#getSortObject() - * sorting} is not supported by {@code db.collection.updateOne}. Use - * {@link #findAndModify(Query, UpdateDefinition, Class)} instead. + * auto-incremented if not explicitly specified in the update. * - * @param query the query document that specifies the criteria used to select a document to be updated. Must not be - * {@literal null}. + * @param query the query document that specifies the criteria used to select a document to be updated. The + * {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to update when + * potentially matching multiple candidates. 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}. * @param entityClass class that determines the collection to use. @@ -1525,12 +1525,11 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * the provided updated document.
* NOTE: Any additional support for field mapping, versions, etc. is not available due to the lack of * domain type information. Use {@link #updateFirst(Query, UpdateDefinition, Class, String)} to get full type specific - * support.
- * NOTE: {@link Query#getSortObject() sorting} is not supported by {@code db.collection.updateOne}. - * Use {@link #findAndModify(Query, UpdateDefinition, Class, String)} instead. + * support. * - * @param query the query document that specifies the criteria used to select a document to be updated. Must not be - * {@literal null}. + * @param query the query document that specifies the criteria used to select a document to be updated. The + * {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to update when + * potentially matching multiple candidates. 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}. * @param collectionName name of the collection to update the object in. Must not be {@literal null}. @@ -1548,8 +1547,9 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * A potential {@link org.springframework.data.annotation.Version} property of the {@literal entityClass} will be * auto-incremented if not explicitly specified in the update. * - * @param query the query document that specifies the criteria used to select a document to be updated. Must not be - * {@literal null}. + * @param query the query document that specifies the criteria used to select a document to be updated. The + * {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to update when + * potentially matching multiple candidates. 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}. * @param entityClass class of the pojo to be operated on. Must not be {@literal null}. @@ -1745,7 +1745,8 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * * @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}. + * to use. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to + * replace when potentially matching multiple candidates. 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 @@ -1762,7 +1763,8 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * * @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}. + * to use. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to + * replace when potentially matching multiple candidates. 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. @@ -1778,7 +1780,8 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * * @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}. + * to use. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to + * replace when potentially matching multiple candidates. 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. @@ -1796,7 +1799,8 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * * @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}. + * to use. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence which document to + * replace when potentially matching multiple candidates. 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. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java index 47ef05a07..f0e7eb67b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.Sort.Direction.DESC; import java.util.ArrayList; import java.util.Arrays; @@ -30,17 +31,20 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.BulkOperationException; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.convert.UpdateMapper; +import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import org.springframework.data.mongodb.test.util.MongoTemplateExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.Template; @@ -323,6 +327,39 @@ public class DefaultBulkOperationsIntegrationTests { assertThat(doc).isInstanceOf(SpecialDoc.class); } + @Test // GH-4797 + @EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") + public void updateShouldConsiderSorting() { + + insertSomeDocuments(); + + BulkWriteResult result = createBulkOps(BulkMode.ORDERED, BaseDocWithRenamedField.class) + .updateOne(new Query().with(Sort.by(DESC, "renamedField")), new Update().set("bsky", "altnps")).execute(); + + assertThat(result.getModifiedCount()).isOne(); + + Document raw = operations.execute(COLLECTION_NAME, col -> col.find(new Document("_id", "4")).first()); + assertThat(raw).containsEntry("bsky", "altnps"); + } + + @Test // GH-4797 + @EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") + public void replaceShouldConsiderSorting() { + + insertSomeDocuments(); + + BaseDocWithRenamedField target = new BaseDocWithRenamedField(); + target.value = "replacement"; + + BulkWriteResult result = createBulkOps(BulkMode.ORDERED, BaseDocWithRenamedField.class) + .replaceOne(new Query().with(Sort.by(DESC, "renamedField")), target).execute(); + + assertThat(result.getModifiedCount()).isOne(); + + Document raw = operations.execute(COLLECTION_NAME, col -> col.find(new Document("_id", "4")).first()); + assertThat(raw).containsEntry("value", target.value); + } + private void testUpdate(BulkMode mode, boolean multi, int expectedUpdates) { BulkOperations bulkOps = createBulkOps(mode); @@ -384,10 +421,10 @@ public class DefaultBulkOperationsIntegrationTests { final MongoCollection coll = operations.getCollection(COLLECTION_NAME); - coll.insertOne(rawDoc("1", "value1")); - coll.insertOne(rawDoc("2", "value1")); - coll.insertOne(rawDoc("3", "value2")); - coll.insertOne(rawDoc("4", "value2")); + coll.insertOne(rawDoc("1", "value1").append("rn_f", "001")); + coll.insertOne(rawDoc("2", "value1").append("rn_f", "002")); + coll.insertOne(rawDoc("3", "value2").append("rn_f", "003")); + coll.insertOne(rawDoc("4", "value2").append("rn_f", "004")); } private static Stream upsertArguments() { @@ -421,4 +458,10 @@ public class DefaultBulkOperationsIntegrationTests { private static Document rawDoc(String id, String value) { return new Document("_id", id).append("value", value); } + + static class BaseDocWithRenamedField extends BaseDoc { + + @Field("rn_f") + String renamedField; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperationsTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperationsTests.java index 90fdaabb3..79bf56315 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperationsTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperationsTests.java @@ -16,7 +16,11 @@ package org.springframework.data.mongodb.core; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.Sort.Direction.DESC; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; @@ -289,11 +293,48 @@ class DefaultReactiveBulkOperationsTests { }).verifyComplete(); } + @Test // GH-4797 + @EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") + public void updateShouldConsiderSorting() { + + insertSomeDocuments(); + + createBulkOps(BulkMode.ORDERED, BaseDocWithRenamedField.class) // + .updateOne(new Query().with(Sort.by(DESC, "renamedField")), new Update().set("bsky", "altnps")).execute() // + .as(StepVerifier::create) // + .consumeNextWith(result -> assertThat(result.getModifiedCount()).isOne()) // + .verifyComplete(); + + template.execute(COLLECTION_NAME, col -> col.find(new Document("_id", "4")).first()).as(StepVerifier::create) // + .consumeNextWith(raw -> assertThat(raw).containsEntry("bsky", "altnps")) // + .verifyComplete(); + } + + @Test // GH-4797 + @EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") + public void replaceShouldConsiderSorting() { + + insertSomeDocuments(); + + BaseDocWithRenamedField target = new BaseDocWithRenamedField(); + target.value = "replacement"; + + createBulkOps(BulkMode.ORDERED, BaseDocWithRenamedField.class) // + .replaceOne(new Query().with(Sort.by(DESC, "renamedField")), target).execute() // + .as(StepVerifier::create) // + .consumeNextWith(result -> assertThat(result.getModifiedCount()).isOne()) // + .verifyComplete(); + + template.execute(COLLECTION_NAME, col -> col.find(new Document("_id", "4")).first()).as(StepVerifier::create) // + .consumeNextWith(raw -> assertThat(raw).containsEntry("value", target.value)) // + .verifyComplete(); + } + private void insertSomeDocuments() { template.execute(COLLECTION_NAME, collection -> { return Flux.from(collection.insertMany( - List.of(rawDoc("1", "value1"), rawDoc("2", "value1"), rawDoc("3", "value2"), rawDoc("4", "value2")))); + List.of(rawDoc("1", "value1").append("rn_f", "001"), rawDoc("2", "value1").append("rn_f", "002"), rawDoc("3", "value2").append("rn_f", "003"), rawDoc("4", "value2").append("rn_f", "004")))); }).then().as(StepVerifier::create).verifyComplete(); } @@ -345,4 +386,10 @@ class DefaultReactiveBulkOperationsTests { private static Document rawDoc(String id, String value) { return new Document("_id", id).append("value", value); } + + static class BaseDocWithRenamedField extends BaseDoc { + + @Field("rn_f") + String renamedField; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateReplaceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateReplaceTests.java index 6fe9eddc9..6b8e158e5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateReplaceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateReplaceTests.java @@ -34,9 +34,13 @@ 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.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.test.util.Client; +import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import org.springframework.data.mongodb.test.util.MongoClientExtension; import com.mongodb.client.MongoClient; @@ -171,6 +175,17 @@ public class MongoTemplateReplaceTests { assertThat(document).containsEntry("r-name", "Pizza Rat's Pizzaria"); } + @Test // GH-4797 + @EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") + void replaceConsidersSort() { + + UpdateResult result = template.replace(new Query().with(Sort.by(Direction.DESC, "name")), new Restaurant("resist", "Manhattan")); + + assertThat(result.getModifiedCount()).isOne(); + Document document = retrieve(collection -> collection.find(Filters.eq("_id", 2)).first()); + assertThat(document).containsEntry("r-name", "resist"); + } + void initTestData() { List testData = Stream.of( // 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 cf9fc09cd..83d4e30cc 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 @@ -4066,64 +4066,6 @@ public class MongoTemplateTests { assertThat(loaded.mapValue).isEqualTo(sourceMap); } - @Test // GH-4797 - public void updateFirstWithSortingAscendingShouldUpdateCorrectEntities() { - - PersonWithIdPropertyOfTypeObjectId youngPerson = new PersonWithIdPropertyOfTypeObjectId(); - youngPerson.setId(new ObjectId()); - youngPerson.setAge(27); - youngPerson.setFirstName("Dave"); - template.save(youngPerson); - - PersonWithIdPropertyOfTypeObjectId oldPerson = new PersonWithIdPropertyOfTypeObjectId(); - oldPerson.setId(new ObjectId()); - oldPerson.setAge(34); - oldPerson.setFirstName("Dave"); - template.save(oldPerson); - - template.updateFirst(query(where("firstName").is("Dave")).with(Sort.by(Direction.ASC, "age")), - update("firstName", "Mike"), PersonWithIdPropertyOfTypeObjectId.class); - - PersonWithIdPropertyOfTypeObjectId oldPersonResult = template.findById(oldPerson.getId(), - PersonWithIdPropertyOfTypeObjectId.class); - assertThat(oldPersonResult).isNotNull(); - assertThat(oldPersonResult.getFirstName()).isEqualTo("Dave"); - - PersonWithIdPropertyOfTypeObjectId youngPersonResult = template.findById(youngPerson.getId(), - PersonWithIdPropertyOfTypeObjectId.class); - assertThat(youngPersonResult).isNotNull(); - assertThat(youngPersonResult.getFirstName()).isEqualTo("Mike"); - } - - @Test // GH-4797 - public void updateFirstWithSortingDescendingShouldUpdateCorrectEntities() { - - PersonWithIdPropertyOfTypeObjectId youngPerson = new PersonWithIdPropertyOfTypeObjectId(); - youngPerson.setId(new ObjectId()); - youngPerson.setAge(27); - youngPerson.setFirstName("Dave"); - template.save(youngPerson); - - PersonWithIdPropertyOfTypeObjectId oldPerson = new PersonWithIdPropertyOfTypeObjectId(); - oldPerson.setId(new ObjectId()); - oldPerson.setAge(34); - oldPerson.setFirstName("Dave"); - template.save(oldPerson); - - template.updateFirst(query(where("firstName").is("Dave")).with(Sort.by(Direction.DESC, "age")), - update("firstName", "Mike"), PersonWithIdPropertyOfTypeObjectId.class); - - PersonWithIdPropertyOfTypeObjectId oldPersonResult = template.findById(oldPerson.getId(), - PersonWithIdPropertyOfTypeObjectId.class); - assertThat(oldPersonResult).isNotNull(); - assertThat(oldPersonResult.getFirstName()).isEqualTo("Mike"); - - PersonWithIdPropertyOfTypeObjectId youngPersonResult = template.findById(youngPerson.getId(), - PersonWithIdPropertyOfTypeObjectId.class); - assertThat(youngPersonResult).isNotNull(); - assertThat(youngPersonResult.getFirstName()).isEqualTo("Dave"); - } - private AtomicReference createAfterSaveReference() { AtomicReference saved = new AtomicReference<>(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUpdateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUpdateTests.java index 8ae8f01aa..10a0202d9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUpdateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUpdateTests.java @@ -15,19 +15,26 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; +import com.mongodb.client.result.UpdateResult; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators; import org.springframework.data.mongodb.core.aggregation.ReplaceWithOperation; @@ -37,6 +44,7 @@ import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import org.springframework.data.mongodb.test.util.MongoTemplateExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; @@ -123,13 +131,13 @@ class MongoTemplateUpdateTests { Versioned source = template.insert(Versioned.class).one(new Versioned("id-1", "value-0")); AggregationUpdate update = AggregationUpdate.update() - .set(SetOperation.builder().set("value").toValue("changed").and().set("version").toValue(10L)); + .set(SetOperation.builder().set("value").toValue("changed").and().set("version").toValue(10L)); template.update(Versioned.class).matching(Query.query(Criteria.where("id").is(source.id))).apply(update).first(); assertThat( - collection(Versioned.class).find(new org.bson.Document("_id", source.id)).limit(1).into(new ArrayList<>())) - .containsExactly(new org.bson.Document("_id", source.id).append("version", 10L).append("value", "changed") - .append("_class", "org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Versioned")); + collection(Versioned.class).find(new org.bson.Document("_id", source.id)).limit(1).into(new ArrayList<>())) + .containsExactly(new org.bson.Document("_id", source.id).append("version", 10L).append("value", "changed") + .append("_class", "org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Versioned")); } @Test // DATAMONGO-2331 @@ -289,6 +297,35 @@ class MongoTemplateUpdateTests { null); } + @ParameterizedTest // GH-4797 + @EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") + @MethodSource("sortedUpdateBookArgs") + void updateFirstWithSort(Class domainType, Sort sort, UpdateDefinition update) { + + Book one = new Book(); + one.id = 1; + one.isbn = "001 001 300"; + one.title = "News isn't fake"; + one.author = new Author("John", "Backus"); + + Book two = new Book(); + two.id = 2; + two.title = "love is love"; + two.isbn = "001 001 100"; + two.author = new Author("Grace", "Hopper"); + + template.insertAll(Arrays.asList(one, two)); + + UpdateResult result = template.update(domainType) // + .inCollection(template.getCollectionName(Book.class))// + .matching(new Query().with(sort)).apply(update) // + .first(); + + assertThat(result.getModifiedCount()).isOne(); + assertThat(collection(Book.class).find(new org.bson.Document("_id", two.id)).first()).containsEntry("title", + "Science is real!"); + } + private List all(Class type) { return collection(type).find(new org.bson.Document()).into(new ArrayList<>()); } @@ -297,6 +334,21 @@ class MongoTemplateUpdateTests { return template.getCollection(template.getCollectionName(type)); } + private static Stream sortedUpdateBookArgs() { + + Update update = new Update().set("title", "Science is real!"); + AggregationUpdate aggUpdate = AggregationUpdate.update().set("title").toValue("Science is real!"); + + return Stream.of( // + Arguments.of(Book.class, Sort.by(Direction.ASC, "isbn"), update), // typed, no field mapping + Arguments.of(Book.class, Sort.by(Direction.DESC, "author.lastname"), update), // typed, map `lastname` + Arguments.of(Book.class, Sort.by(Direction.DESC, "author.last"), update), // typed, raw field name + Arguments.of(Object.class, Sort.by(Direction.ASC, "isbn"), update), // untyped, requires raw field name + Arguments.of(Book.class, Sort.by(Direction.ASC, "isbn"), aggUpdate), // aggregation, no field mapping + Arguments.of(Book.class, Sort.by(Direction.DESC, "author.last"), aggUpdate) // aggregation, map `lastname` + ); + } + @Document("scores") static class Score { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateReplaceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateReplaceTests.java index f2d126b58..86433ab33 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateReplaceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateReplaceTests.java @@ -20,6 +20,7 @@ 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 org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -37,7 +38,10 @@ 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.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.MongoClientExtension; @@ -198,7 +202,23 @@ public class ReactiveMongoTemplateReplaceTests { retrieve(collection -> collection.find(Filters.eq("_id", 4)).first()).as(StepVerifier::create) .consumeNextWith(document -> { assertThat(document).containsEntry("r-name", "Pizza Rat's Pizzaria"); - }); + }) + .verifyComplete(); + } + + @Test // GH-4797 + @EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") + void replaceConsidersSort() { + + template.replace(new Query().with(Sort.by(Direction.DESC, "name")), new Restaurant("resist", "Manhattan")) // + .as(StepVerifier::create) // + .consumeNextWith(result -> assertThat(result.getModifiedCount()).isOne()) // + .verifyComplete(); + + retrieve(collection -> collection.find(Filters.eq("_id", 2)).first()).as(StepVerifier::create) + .consumeNextWith(document -> { + assertThat(document).containsEntry("r-name", "resist"); + }).verifyComplete(); } void initTestData() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java index c130436a5..80dd584b9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java @@ -1846,42 +1846,6 @@ public class ReactiveMongoTemplateTests { .verify(); } - @Test // GH-4797 - public void updateFirstWithSortingAscendingShouldUpdateCorrectEntities() { - - Person youngPerson = new Person("Dave", 27); - Person oldPerson = new Person("Dave", 34); - - template.insertAll(List.of(youngPerson, oldPerson)) - .then(template.updateFirst(new Query(where("firstName").is("Dave")).with(Sort.by(Direction.ASC, "age")), - new Update().set("firstName", "Carter"), Person.class)) - .flatMapMany(p -> template.find(new Query().with(Sort.by(Direction.ASC, "age")), Person.class)) - .as(StepVerifier::create) // - .consumeNextWith(actual -> { - assertThat(actual.getFirstName()).isEqualTo("Carter"); - }).consumeNextWith(actual -> { - assertThat(actual.getFirstName()).isEqualTo("Dave"); - }).verifyComplete(); - } - - @Test // GH-4797 - public void updateFirstWithSortingDescendingShouldUpdateCorrectEntities() { - - Person youngPerson = new Person("Dave", 27); - Person oldPerson = new Person("Dave", 34); - - template.insertAll(List.of(youngPerson, oldPerson)) - .then(template.updateFirst(new Query(where("firstName").is("Dave")).with(Sort.by(Direction.DESC, "age")), - new Update().set("firstName", "Carter"), Person.class)) - .flatMapMany(p -> template.find(new Query().with(Sort.by(Direction.ASC, "age")), Person.class)) - .as(StepVerifier::create) // - .consumeNextWith(actual -> { - assertThat(actual.getFirstName()).isEqualTo("Dave"); - }).consumeNextWith(actual -> { - assertThat(actual.getFirstName()).isEqualTo("Carter"); - }).verifyComplete(); - } - private PersonWithAList createPersonWithAList(String firstname, int age) { PersonWithAList p = new PersonWithAList(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUpdateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUpdateTests.java index a89e2eff1..35c27815f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUpdateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUpdateTests.java @@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core; import static org.assertj.core.api.Assertions.*; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.ArrayList; @@ -25,12 +26,18 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators; import org.springframework.data.mongodb.core.aggregation.ReplaceWithOperation; @@ -39,6 +46,8 @@ import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import org.springframework.data.mongodb.test.util.MongoClientExtension; @@ -269,6 +278,39 @@ public class ReactiveMongoTemplateUpdateTests { } + @ParameterizedTest // GH-4797 + @EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") + @MethodSource("sortedUpdateBookArgs") + void updateFirstWithSort(Class domainType, Sort sort, UpdateDefinition update) { + + Book one = new Book(); + one.id = 1; + one.isbn = "001 001 300"; + one.title = "News isn't fake"; + one.author = new Author("John", "Backus"); + + Book two = new Book(); + two.id = 2; + two.title = "love is love"; + two.isbn = "001 001 100"; + two.author = new Author("Grace", "Hopper"); + + template.insertAll(Arrays.asList(one, two)).then().as(StepVerifier::create).verifyComplete(); + + template.update(domainType) // + .inCollection(template.getCollectionName(Book.class))// + .matching(new Query().with(sort)).apply(update) // + .first().as(StepVerifier::create) // + .assertNext(result -> assertThat(result.getModifiedCount()).isOne()) // + .verifyComplete(); + + Mono.from(collection(Book.class).find(new org.bson.Document("_id", two.id)).first()) // + .as(StepVerifier::create) // + .assertNext(document -> assertThat(document).containsEntry("title", "Science is real!")) // + .verifyComplete(); + } + + private Flux all(Class type) { return Flux.from(collection(type).find(new org.bson.Document())); } @@ -277,6 +319,21 @@ public class ReactiveMongoTemplateUpdateTests { return client.getDatabase(DB_NAME).getCollection(template.getCollectionName(type)); } + private static Stream sortedUpdateBookArgs() { + + Update update = new Update().set("title", "Science is real!"); + AggregationUpdate aggUpdate = AggregationUpdate.update().set("title").toValue("Science is real!"); + + return Stream.of( // + Arguments.of(Book.class, Sort.by(Direction.ASC, "isbn"), update), // typed, no field mapping + Arguments.of(Book.class, Sort.by(Direction.DESC, "author.lastname"), update), // typed, map `lastname` + Arguments.of(Book.class, Sort.by(Direction.DESC, "author.last"), update), // typed, raw field name + Arguments.of(Object.class, Sort.by(Direction.ASC, "isbn"), update), // untyped, requires raw field name + Arguments.of(Book.class, Sort.by(Direction.ASC, "isbn"), aggUpdate), // aggregation, no field mapping + Arguments.of(Book.class, Sort.by(Direction.DESC, "author.last"), aggUpdate) // aggregation, map `lastname` + ); + } + @Document("scores") static class Score { diff --git a/src/main/antora/modules/ROOT/pages/mongodb/template-crud-operations.adoc b/src/main/antora/modules/ROOT/pages/mongodb/template-crud-operations.adoc index c0271ee7b..491bb4ab7 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/template-crud-operations.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/template-crud-operations.adoc @@ -356,7 +356,7 @@ Read more in the see xref:mongodb/template-crud-operations.adoc#mongo-template.o * *updateFirst*: Updates the first document that matches the query document criteria with the updated document. * *updateMulti*: Updates all objects that match the query document criteria with the updated document. -WARNING: `updateFirst` does not support ordering. Please use xref:mongodb/template-crud-operations.adoc#mongo-template.find-and-upsert[findAndModify] to apply `Sort`. +WARNING: `updateFirst` does not support ordering for MongoDB Versions below 8.0. Running one of the older versions, please use xref:mongodb/template-crud-operations.adoc#mongo-template.find-and-upsert[findAndModify] to apply `Sort`. NOTE: Index hints for the update operation can be provided via `Query.withHint(...)`.