Browse Source

Apply sort to replace and bulk operation updates

Allow using sort parameter from the query for template replace as well as bulk update & replace operations.
We now also mapped fields used in sort to the domain type considering field annotations.
Also updated javadoc and reference documentation.

Original Pull Request: #4888
pull/4915/head
Christoph Strobl 10 months ago
parent
commit
d79031b60d
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 13
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperations.java
  2. 24
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperationsSupport.java
  3. 8
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java
  4. 7
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java
  5. 32
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
  6. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java
  7. 14
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveBulkOperations.java
  8. 36
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
  9. 51
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java
  10. 49
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperationsTests.java
  11. 15
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateReplaceTests.java
  12. 58
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
  13. 54
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUpdateTests.java
  14. 22
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateReplaceTests.java
  15. 36
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java
  16. 57
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUpdateTests.java
  17. 2
      src/main/antora/modules/ROOT/pages/mongodb/template-crud-operations.adoc

13
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 java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.core.query.UpdateDefinition; 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. * 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}. * @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}. * @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. * 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}. * @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}. * @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
* @since 4.1 * @since 4.1
@ -187,7 +190,8 @@ public interface BulkOperations {
/** /**
* Add a single replace operation to the bulk operation. * 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 replacement the replacement document. Must not be {@literal null}.
* @return the current {@link BulkOperations} instance with the replacement added, will never be {@literal null}. * @return the current {@link BulkOperations} instance with the replacement added, will never be {@literal null}.
* @since 2.2 * @since 2.2
@ -199,7 +203,8 @@ public interface BulkOperations {
/** /**
* Add a single replace operation to the bulk operation. * 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 replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. 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}. * @return the current {@link BulkOperations} instance with the replacement added, will never be {@literal null}.

24
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperationsSupport.java

@ -106,6 +106,11 @@ abstract class BulkOperationsSupport {
if (writeModel instanceof UpdateOneModel<Document> model) { if (writeModel instanceof UpdateOneModel<Document> 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) { if (source instanceof AggregationUpdate aggregationUpdate) {
List<Document> pipeline = mapUpdatePipeline(aggregationUpdate); List<Document> pipeline = mapUpdatePipeline(aggregationUpdate);
@ -136,6 +141,17 @@ abstract class BulkOperationsSupport {
return new DeleteManyModel<>(getMappedQuery(model.getFilter()), model.getOptions()); return new DeleteManyModel<>(getMappedQuery(model.getFilter()), model.getOptions());
} }
if (writeModel instanceof ReplaceOneModel<Document> 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; 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 filterQuery The {@link Query} to read a potential {@link Collation} from. Must not be {@literal null}.
* @param update The {@link Update} to apply * @param update The {@link Update} to apply
* @param upsert flag to indicate if document should be upserted. * @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}. * @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(); UpdateOptions options = new UpdateOptions();
options.upsert(upsert); options.upsert(upsert);
@ -207,6 +225,10 @@ abstract class BulkOperationsSupport {
options.arrayFilters(list); options.arrayFilters(list);
} }
if (!multi && filterQuery.isSorted()) {
options.sort(filterQuery.getSortObject());
}
filterQuery.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation); filterQuery.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation);
return options; return options;
} }

8
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 replaceOptions = new ReplaceOptions();
replaceOptions.upsert(options.isUpsert()); replaceOptions.upsert(options.isUpsert());
if (query.isSorted()) {
replaceOptions.sort(query.getSortObject());
}
query.getCollation().map(Collation::toMongoCollation).ifPresent(replaceOptions::collation); query.getCollation().map(Collation::toMongoCollation).ifPresent(replaceOptions::collation);
maybeEmitEvent(new BeforeConvertEvent<>(replacement, collectionName)); maybeEmitEvent(new BeforeConvertEvent<>(replacement, collectionName));
Object source = maybeInvokeBeforeConvertCallback(replacement); Object source = maybeInvokeBeforeConvertCallback(replacement);
addModel(source, addModel(source, new ReplaceOneModel<>(query.getQueryObject(), getMappedObject(source), replaceOptions));
new ReplaceOneModel<>(getMappedQuery(query.getQueryObject()), getMappedObject(source), replaceOptions));
return this; return this;
} }
@ -315,7 +317,7 @@ class DefaultBulkOperations extends BulkOperationsSupport implements BulkOperati
Assert.notNull(query, "Query must not be null"); Assert.notNull(query, "Query must not be null");
Assert.notNull(update, "Update 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) { if (multi) {
addModel(update, new UpdateManyModel<>(query.getQueryObject(), update.getUpdateObject(), options)); addModel(update, new UpdateManyModel<>(query.getQueryObject(), update.getUpdateObject(), options));

7
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 replaceOptions = new ReplaceOptions();
replaceOptions.upsert(options.isUpsert()); replaceOptions.upsert(options.isUpsert());
if (query.isSorted()) {
replaceOptions.sort(query.getSortObject());
}
query.getCollation().map(Collation::toMongoCollation).ifPresent(replaceOptions::collation); query.getCollation().map(Collation::toMongoCollation).ifPresent(replaceOptions::collation);
this.models.add(Mono.just(replacement).flatMap(it -> { this.models.add(Mono.just(replacement).flatMap(it -> {
maybeEmitEvent(new BeforeConvertEvent<>(it, collectionName)); maybeEmitEvent(new BeforeConvertEvent<>(it, collectionName));
return maybeInvokeBeforeConvertCallback(it); return maybeInvokeBeforeConvertCallback(it);
}).map(it -> new SourceAwareWriteModelHolder(it, }).map(it -> new SourceAwareWriteModelHolder(it,
new ReplaceOneModel<>(getMappedQuery(query.getQueryObject()), getMappedObject(it), replaceOptions)))); new ReplaceOneModel<>(query.getQueryObject(), getMappedObject(it), replaceOptions))));
return this; return this;
} }
@ -265,7 +268,7 @@ class DefaultReactiveBulkOperations extends BulkOperationsSupport implements Rea
Assert.notNull(query, "Query must not be null"); Assert.notNull(query, "Query must not be null");
Assert.notNull(update, "Update 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 -> { this.models.add(Mono.just(update).map(it -> {
if (multi) { if (multi) {

32
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.bson.Document;
import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window; import org.springframework.data.domain.Window;
import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.GeoResults;
import org.springframework.data.mongodb.core.BulkOperations.BulkMode; 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 * A potential {@link org.springframework.data.annotation.Version} property of the {@literal entityClass} will be
* auto-incremented if not explicitly specified in the update. * 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 * @param query the query document that specifies the criteria used to select a document to be updated. The
* {@literal null}. * {@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 * @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}. * the existing. Must not be {@literal null}.
* @param entityClass class that determines the collection to use. * @param entityClass class that determines the collection to use.
@ -1623,12 +1625,11 @@ public interface MongoOperations extends FluentMongoOperations {
* the provided updated document. <br /> * the provided updated document. <br />
* <strong>NOTE:</strong> Any additional support for field mapping, versions, etc. is not available due to the lack of * <strong>NOTE:</strong> 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 * domain type information. Use {@link #updateFirst(Query, UpdateDefinition, Class, String)} to get full type specific
* support. <br /> * support.
* <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 document to be updated. Must not be * @param query the query document that specifies the criteria used to select a document to be updated. The
* {@literal null}. * {@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 * @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}. * the existing. Must not be {@literal null}.
* @param collectionName name of the collection to update the object in. 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 * A potential {@link org.springframework.data.annotation.Version} property of the {@literal entityClass} will be auto
* incremented if not explicitly specified in the update. * 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 * @param query the query document that specifies the criteria used to select a document to be updated. The
* {@literal null}. * {@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 * @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}. * the existing. Must not be {@literal null}.
* @param entityClass class of the pojo to be operated on. 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 * @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} * 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 replacement the replacement document. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement. * @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 * @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 * @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} * 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 replacement the replacement document. Must not be {@literal null}.
* @param collectionName the collection to query. 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. * @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 * @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} * 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 replacement the replacement document. Must not be {@literal null}.
* @param options the {@link ReplaceOptions} holding additional information. 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. * @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 * * @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} * 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 replacement the replacement document. Must not be {@literal null}.
* @param options the {@link ReplaceOptions} holding additional information. 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. * @return the {@link UpdateResult} which lets you access the results of the previous replacement.

5
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java

@ -765,7 +765,7 @@ class QueryOperations {
} }
if (query != null && query.isSorted()) { 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); HintFunction.from(getQuery().getHint()).ifPresent(codecRegistryProvider, options::hintString, options::hint);
@ -799,6 +799,9 @@ class QueryOperations {
options.collation(updateOptions.getCollation()); options.collation(updateOptions.getCollation());
options.upsert(updateOptions.isUpsert()); options.upsert(updateOptions.isUpsert());
applyHint(options::hintString, options::hint); applyHint(options::hintString, options::hint);
if (!isMulti() && getQuery().isSorted()) {
options.sort(getMappedSort(domainType != null ? mappingContext.getPersistentEntity(domainType) : null));
}
if (callback != null) { if (callback != null) {
callback.accept(options); callback.accept(options);

14
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 java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UpdateDefinition; 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. * 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}. * @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}. * @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. * 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
* @param replacement the replacement document. Must not be {@literal null}. * 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}. * @return the current {@link ReactiveBulkOperations} instance with the replace added, will never be {@literal null}.
*/ */
default ReactiveBulkOperations replaceOne(Query query, Object replacement) { default ReactiveBulkOperations replaceOne(Query query, Object replacement) {
@ -122,7 +127,8 @@ public interface ReactiveBulkOperations {
/** /**
* Add a single replace operation to the bulk operation. * 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 replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. 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}. * @return the current {@link ReactiveBulkOperations} instance with the replace added, will never be {@literal null}.

36
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.Publisher;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window; import org.springframework.data.domain.Window;
import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResult;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
@ -1502,12 +1503,11 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* the provided update document. * the provided update document.
* <p> * <p>
* A potential {@link org.springframework.data.annotation.Version} property of the {@literal entityClass} will be * A potential {@link org.springframework.data.annotation.Version} property of the {@literal entityClass} will be
* auto-incremented if not explicitly specified in the update. <strong>NOTE:</strong> {@link Query#getSortObject() * auto-incremented if not explicitly specified in the update.
* 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 document to be updated. Must not be * @param query the query document that specifies the criteria used to select a document to be updated. The
* {@literal null}. * {@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 * @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}. * the existing. Must not be {@literal null}.
* @param entityClass class that determines the collection to use. * @param entityClass class that determines the collection to use.
@ -1525,12 +1525,11 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* the provided updated document. <br /> * the provided updated document. <br />
* <strong>NOTE:</strong> Any additional support for field mapping, versions, etc. is not available due to the lack of * <strong>NOTE:</strong> 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 * domain type information. Use {@link #updateFirst(Query, UpdateDefinition, Class, String)} to get full type specific
* support. <br /> * support.
* <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 document to be updated. Must not be * @param query the query document that specifies the criteria used to select a document to be updated. The
* {@literal null}. * {@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 * @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}. * the existing. Must not be {@literal null}.
* @param collectionName name of the collection to update the object in. 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 * A potential {@link org.springframework.data.annotation.Version} property of the {@literal entityClass} will be
* auto-incremented if not explicitly specified in the update. * 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 * @param query the query document that specifies the criteria used to select a document to be updated. The
* {@literal null}. * {@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 * @param update the {@link UpdateDefinition} that contains the updated object or {@code $} operators to manipulate
* the existing. Must not be {@literal null}. * the existing. Must not be {@literal null}.
* @param entityClass class of the pojo to be operated on. 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 * @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} * 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 replacement the replacement document. Must not be {@literal null}.
* @return the {@link UpdateResult} which lets you access the results of the previous replacement. * @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 * @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 * @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} * 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 replacement the replacement document. Must not be {@literal null}.
* @param collectionName the collection to query. 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. * @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 * @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} * 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 replacement the replacement document. Must not be {@literal null}.
* @param options the {@link ReplaceOptions} holding additional information. 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. * @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 * * @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} * 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 replacement the replacement document. Must not be {@literal null}.
* @param options the {@link ReplaceOptions} holding additional information. 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. * @return the {@link UpdateResult} which lets you access the results of the previous replacement.

51
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java

@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core; package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.domain.Sort.Direction.DESC;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; 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.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.BulkOperationException; import org.springframework.data.mongodb.BulkOperationException;
import org.springframework.data.mongodb.core.BulkOperations.BulkMode; import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext; import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext;
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper; 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.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.core.query.UpdateDefinition; 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.MongoTemplateExtension;
import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.MongoTestTemplate;
import org.springframework.data.mongodb.test.util.Template; import org.springframework.data.mongodb.test.util.Template;
@ -323,6 +327,39 @@ public class DefaultBulkOperationsIntegrationTests {
assertThat(doc).isInstanceOf(SpecialDoc.class); 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) { private void testUpdate(BulkMode mode, boolean multi, int expectedUpdates) {
BulkOperations bulkOps = createBulkOps(mode); BulkOperations bulkOps = createBulkOps(mode);
@ -384,10 +421,10 @@ public class DefaultBulkOperationsIntegrationTests {
final MongoCollection<Document> coll = operations.getCollection(COLLECTION_NAME); final MongoCollection<Document> coll = operations.getCollection(COLLECTION_NAME);
coll.insertOne(rawDoc("1", "value1")); coll.insertOne(rawDoc("1", "value1").append("rn_f", "001"));
coll.insertOne(rawDoc("2", "value1")); coll.insertOne(rawDoc("2", "value1").append("rn_f", "002"));
coll.insertOne(rawDoc("3", "value2")); coll.insertOne(rawDoc("3", "value2").append("rn_f", "003"));
coll.insertOne(rawDoc("4", "value2")); coll.insertOne(rawDoc("4", "value2").append("rn_f", "004"));
} }
private static Stream<Arguments> upsertArguments() { private static Stream<Arguments> upsertArguments() {
@ -421,4 +458,10 @@ public class DefaultBulkOperationsIntegrationTests {
private static Document rawDoc(String id, String value) { private static Document rawDoc(String id, String value) {
return new Document("_id", id).append("value", value); return new Document("_id", id).append("value", value);
} }
static class BaseDocWithRenamedField extends BaseDoc {
@Field("rn_f")
String renamedField;
}
} }

49
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperationsTests.java

@ -16,7 +16,11 @@
package org.springframework.data.mongodb.core; package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*; 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.core.publisher.Flux;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
@ -289,11 +293,48 @@ class DefaultReactiveBulkOperationsTests {
}).verifyComplete(); }).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() { private void insertSomeDocuments() {
template.execute(COLLECTION_NAME, collection -> { template.execute(COLLECTION_NAME, collection -> {
return Flux.from(collection.insertMany( 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(); }).then().as(StepVerifier::create).verifyComplete();
} }
@ -345,4 +386,10 @@ class DefaultReactiveBulkOperationsTests {
private static Document rawDoc(String id, String value) { private static Document rawDoc(String id, String value) {
return new Document("_id", id).append("value", value); return new Document("_id", id).append("value", value);
} }
static class BaseDocWithRenamedField extends BaseDoc {
@Field("rn_f")
String renamedField;
}
} }

15
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.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.dao.DataIntegrityViolationException; 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.aggregation.AggregationUpdate;
import org.springframework.data.mongodb.core.mapping.Field; 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.Client;
import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.MongoClientExtension;
import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClient;
@ -171,6 +175,17 @@ public class MongoTemplateReplaceTests {
assertThat(document).containsEntry("r-name", "Pizza Rat's Pizzaria"); 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() { void initTestData() {
List<Document> testData = Stream.of( // List<Document> testData = Stream.of( //

58
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); 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<ImmutableVersioned> createAfterSaveReference() { private AtomicReference<ImmutableVersioned> createAfterSaveReference() {
AtomicReference<ImmutableVersioned> saved = new AtomicReference<>(); AtomicReference<ImmutableVersioned> saved = new AtomicReference<>();

54
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUpdateTests.java

@ -15,19 +15,26 @@
*/ */
package org.springframework.data.mongodb.core; 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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; 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.Id;
import org.springframework.data.annotation.Version; 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.AggregationUpdate;
import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators; import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators;
import org.springframework.data.mongodb.core.aggregation.ReplaceWithOperation; 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.Criteria;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update; 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.EnableIfMongoServerVersion;
import org.springframework.data.mongodb.test.util.MongoTemplateExtension; import org.springframework.data.mongodb.test.util.MongoTemplateExtension;
import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.MongoTestTemplate;
@ -289,6 +297,35 @@ class MongoTemplateUpdateTests {
null); 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<org.bson.Document> all(Class<?> type) { private List<org.bson.Document> all(Class<?> type) {
return collection(type).find(new org.bson.Document()).into(new ArrayList<>()); return collection(type).find(new org.bson.Document()).into(new ArrayList<>());
} }
@ -297,6 +334,21 @@ class MongoTemplateUpdateTests {
return template.getCollection(template.getCollectionName(type)); return template.getCollection(template.getCollectionName(type));
} }
private static Stream<Arguments> 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") @Document("scores")
static class Score { static class Score {

22
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.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*; import static org.springframework.data.mongodb.core.query.Query.*;
import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
@ -37,7 +38,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.springframework.dao.DataIntegrityViolationException; 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.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.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension; 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) retrieve(collection -> collection.find(Filters.eq("_id", 4)).first()).as(StepVerifier::create)
.consumeNextWith(document -> { .consumeNextWith(document -> {
assertThat(document).containsEntry("r-name", "Pizza Rat's Pizzaria"); 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() { void initTestData() {

36
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java

@ -1846,42 +1846,6 @@ public class ReactiveMongoTemplateTests {
.verify(); .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) { private PersonWithAList createPersonWithAList(String firstname, int age) {
PersonWithAList p = new PersonWithAList(); PersonWithAList p = new PersonWithAList();

57
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 static org.assertj.core.api.Assertions.*;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import java.util.ArrayList; import java.util.ArrayList;
@ -25,12 +26,18 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; 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.Id;
import org.springframework.data.annotation.Version; 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.AggregationUpdate;
import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators; import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators;
import org.springframework.data.mongodb.core.aggregation.ReplaceWithOperation; 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.mapping.Field;
import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query; 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.Client;
import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
import org.springframework.data.mongodb.test.util.MongoClientExtension; 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<org.bson.Document> all(Class<?> type) { private Flux<org.bson.Document> all(Class<?> type) {
return Flux.from(collection(type).find(new org.bson.Document())); 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)); return client.getDatabase(DB_NAME).getCollection(template.getCollectionName(type));
} }
private static Stream<Arguments> 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") @Document("scores")
static class Score { static class Score {

2
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. * *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. * *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(...)`. NOTE: Index hints for the update operation can be provided via `Query.withHint(...)`.

Loading…
Cancel
Save