Browse Source

Support QueryResultConverter for reactive delete, replace and update operations.

Original Pull Request: #4949
pull/4976/head
Christoph Strobl 7 months ago committed by Mark Paluch
parent
commit
50aca14d1f
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java
  2. 1
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java
  3. 90
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  4. 31
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java
  5. 25
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java
  6. 25
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java
  7. 40
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java
  8. 16
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java
  9. 32
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java
  10. 13
      src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java

@ -55,6 +55,10 @@ public interface ExecutableRemoveOperation {
*/ */
<T> ExecutableRemove<T> remove(Class<T> domainType); <T> ExecutableRemove<T> remove(Class<T> domainType);
/**
* @author Christoph Strobl
* @since 5.0
*/
interface TerminatingResults<T> { interface TerminatingResults<T> {
/** /**

1
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java

@ -109,6 +109,7 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation {
} }
@Override @Override
@SuppressWarnings({"unchecked", "rawtypes"})
public <R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter) { public <R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter) {
return new ExecutableRemoveSupport<>(template, (Class) domainType, query, collection, converter); return new ExecutableRemoveSupport<>(template, (Class) domainType, query, collection, converter);
} }

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

@ -47,7 +47,6 @@ import org.bson.types.ObjectId;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
@ -114,6 +113,7 @@ import org.springframework.data.mongodb.core.mapping.event.*;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Meta;
import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
@ -1137,9 +1137,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return findAndModify(query, update, options, entityClass, getCollectionName(entityClass)); return findAndModify(query, update, options, entityClass, getCollectionName(entityClass));
} }
@Override public <S, T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<S> entityClass, String collectionName, QueryResultConverter<? super S, ? extends T> resultConverter) {
Class<T> entityClass, String collectionName) {
Assert.notNull(options, "Options must not be null "); Assert.notNull(options, "Options must not be null ");
Assert.notNull(entityClass, "Entity class must not be null"); Assert.notNull(entityClass, "Entity class must not be null");
@ -1156,13 +1155,27 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
} }
return doFindAndModify(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), return doFindAndModify(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(),
query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse,
resultConverter);
}
@Override
public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
Class<T> entityClass, String collectionName) {
return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity());
} }
@Override @Override
@SuppressWarnings("NullAway")
public <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType, public <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
String collectionName, Class<T> resultType) { String collectionName, Class<T> resultType) {
return findAndReplace(query, replacement, options, entityType, collectionName, resultType,
QueryResultConverter.entity());
}
@SuppressWarnings("NullAway")
public <S, T, R> Mono<R> findAndReplace(Query query, S replacement, FindAndReplaceOptions options,
Class<S> entityType, String collectionName, Class<T> resultType,
QueryResultConverter<? super T, ? extends R> resultConverter) {
Assert.notNull(query, "Query must not be null"); Assert.notNull(query, "Query must not be null");
Assert.notNull(replacement, "Replacement must not be null"); Assert.notNull(replacement, "Replacement must not be null");
@ -1199,9 +1212,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
mapped.getCollection())); mapped.getCollection()));
}).flatMap(it -> { }).flatMap(it -> {
Mono<T> afterFindAndReplace = doFindAndReplace(it.getCollection(), collectionPreparer, mappedQuery, Mono<R> afterFindAndReplace = doFindAndReplace(it.getCollection(), collectionPreparer, mappedQuery,
mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(), mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(),
options, projection); options, projection, resultConverter);
return afterFindAndReplace.flatMap(saved -> { return afterFindAndReplace.flatMap(saved -> {
maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), it.getCollection())); maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), it.getCollection()));
return maybeCallAfterSave(saved, it.getTarget(), it.getCollection()); return maybeCallAfterSave(saved, it.getTarget(), it.getCollection());
@ -2280,6 +2293,43 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
.flatMapSequential(deleteResult -> Flux.fromIterable(list))); .flatMapSequential(deleteResult -> Flux.fromIterable(list)));
} }
<S, T> Flux<T> doFindAndDelete(String collectionName, Query query, Class<S> entityClass,
QueryResultConverter<? super S, ? extends T> resultConverter) {
List<Object> ids = new ArrayList<>();
ProjectingReadCallback readCallback = new ProjectingReadCallback(getConverter(),
EntityProjection.nonProjecting(entityClass), collectionName);
QueryResultConverterCallback<S, T> callback = new QueryResultConverterCallback<>(resultConverter, readCallback) {
@Override
public Mono<T> doWith(Document object) {
ids.add(object.get("_id"));
return super.doWith(object);
}
};
Flux<T> flux = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(),
query.getFieldsObject(), entityClass,
new QueryFindPublisherPreparer(query, query.getSortObject(), query.getLimit(), query.getSkip(), entityClass),
callback);
return Flux.from(flux).collectList().filter(it -> !it.isEmpty()).flatMapMany(list -> {
Criteria[] criterias = ids.stream() //
.map(it -> Criteria.where("_id").is(it)) //
.toArray(Criteria[]::new);
Query removeQuery = new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias));
if (query.hasReadPreference()) {
removeQuery.withReadPreference(query.getReadPreference());
}
return Flux.from(remove(removeQuery, entityClass, collectionName))
.flatMapSequential(deleteResult -> Flux.fromIterable(list));
});
}
/** /**
* Create the specified collection using the provided options * Create the specified collection using the provided options
* *
@ -2487,9 +2537,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName);
} }
protected <T> Mono<T> doFindAndModify(String collectionName, <S, T> Mono<T> doFindAndModify(String collectionName,
CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document fields, CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document fields,
@Nullable Document sort, Class<T> entityClass, UpdateDefinition update, FindAndModifyOptions options) { @Nullable Document sort, Class<S> entityClass, UpdateDefinition update, FindAndModifyOptions options,
QueryResultConverter<? super S, ? extends T> resultConverter) {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass); MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
UpdateContext updateContext = queryOperations.updateSingleContext(update, query, false); UpdateContext updateContext = queryOperations.updateSingleContext(update, query, false);
@ -2508,10 +2559,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
serializeToJsonSafely(mappedUpdate), collectionName)); serializeToJsonSafely(mappedUpdate), collectionName));
} }
EntityProjection<S, ?> projection = EntityProjection.nonProjecting(entityClass);
DocumentCallback<T> callback = getResultReader(projection, collectionName, resultConverter);
return executeFindOneInternal( return executeFindOneInternal(
new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate, new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate,
update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options),
new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); callback, collectionName);
}); });
} }
@ -2540,7 +2594,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
EntityProjection<T, ?> projection = operations.introspectProjection(resultType, entityType); EntityProjection<T, ?> projection = operations.introspectProjection(resultType, entityType);
return doFindAndReplace(collectionName, collectionPreparer, mappedQuery, mappedFields, mappedSort, collation, return doFindAndReplace(collectionName, collectionPreparer, mappedQuery, mappedFields, mappedSort, collation,
entityType, replacement, options, projection); entityType, replacement, options, projection, QueryResultConverter.entity());
} }
/** /**
@ -2560,10 +2614,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}.
* @since 3.4 * @since 3.4
*/ */
private <T> Mono<T> doFindAndReplace(String collectionName, private <S, T> Mono<T> doFindAndReplace(String collectionName,
CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document mappedQuery, Document mappedFields, CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document mappedQuery, Document mappedFields,
Document mappedSort, com.mongodb.client.model.Collation collation, Class<?> entityType, Document replacement, Document mappedSort, com.mongodb.client.model.Collation collation, Class<?> entityType, Document replacement,
FindAndReplaceOptions options, EntityProjection<T, ?> projection) { FindAndReplaceOptions options, EntityProjection<S, ?> projection,
QueryResultConverter<? super S, ? extends T> resultConverter) {
return Mono.defer(() -> { return Mono.defer(() -> {
@ -2575,9 +2630,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
serializeToJsonSafely(replacement), collectionName)); serializeToJsonSafely(replacement), collectionName));
} }
DocumentCallback<T> resultReader = getResultReader(projection, collectionName, resultConverter);
return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields, return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields,
mappedSort, replacement, collation, options), mappedSort, replacement, collation, options), resultReader, collectionName);
new ProjectingReadCallback<>(this.mongoConverter, projection, collectionName), collectionName);
}); });
} }
@ -3124,7 +3180,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
FindPublisher<T> doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException; FindPublisher<T> doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException;
} }
static final class QueryResultConverterCallback<T, R> implements DocumentCallback<R> { static class QueryResultConverterCallback<T, R> implements DocumentCallback<R> {
private final QueryResultConverter<? super T, ? extends R> converter; private final QueryResultConverter<? super T, ? extends R> converter;
private final DocumentCallback<T> delegate; private final DocumentCallback<T> delegate;

31
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java

@ -20,6 +20,7 @@ import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Contract;
import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.DeleteResult;
@ -56,16 +57,22 @@ public interface ReactiveRemoveOperation {
<T> ReactiveRemove<T> remove(Class<T> domainType); <T> ReactiveRemove<T> remove(Class<T> domainType);
/** /**
* Compose remove execution by calling one of the terminating methods. * @author Christoph Strobl
* @since 5.0
*/ */
interface TerminatingRemove<T> { interface TerminatingResults<T> {
/** /**
* Remove all documents matching. * Map the query result to a different type using {@link QueryResultConverter}.
* *
* @return {@link Mono} emitting the {@link DeleteResult}. Never {@literal null}. * @param <R> {@link Class type} of the result.
* @param converter the converter, must not be {@literal null}.
* @return new instance of {@link ExecutableFindOperation.TerminatingResults}.
* @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
* @since 5.0
*/ */
Mono<DeleteResult> all(); @Contract("_ -> new")
<R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter);
/** /**
* Remove and return all matching documents. <br/> * Remove and return all matching documents. <br/>
@ -78,6 +85,20 @@ public interface ReactiveRemoveOperation {
Flux<T> findAndRemove(); Flux<T> findAndRemove();
} }
/**
* Compose remove execution by calling one of the terminating methods.
*/
interface TerminatingRemove<T> extends TerminatingResults<T> {
/**
* Remove all documents matching.
*
* @return {@link Mono} emitting the {@link DeleteResult}. Never {@literal null}.
*/
Mono<DeleteResult> all();
}
/** /**
* Collection override (optional). * Collection override (optional).
*/ */

25
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java

@ -15,10 +15,10 @@
*/ */
package org.springframework.data.mongodb.core; package org.springframework.data.mongodb.core;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -47,22 +47,25 @@ class ReactiveRemoveOperationSupport implements ReactiveRemoveOperation {
Assert.notNull(domainType, "DomainType must not be null"); Assert.notNull(domainType, "DomainType must not be null");
return new ReactiveRemoveSupport<>(template, domainType, ALL_QUERY, null); return new ReactiveRemoveSupport<>(template, domainType, ALL_QUERY, null, QueryResultConverter.entity());
} }
static class ReactiveRemoveSupport<T> implements ReactiveRemove<T>, RemoveWithCollection<T> { static class ReactiveRemoveSupport<S, T> implements ReactiveRemove<T>, RemoveWithCollection<T> {
private final ReactiveMongoTemplate template; private final ReactiveMongoTemplate template;
private final Class<T> domainType; private final Class<S> domainType;
private final Query query; private final Query query;
private final @Nullable String collection; private final @Nullable String collection;
private final QueryResultConverter<? super S, ? extends T> resultConverter;
ReactiveRemoveSupport(ReactiveMongoTemplate template, Class<T> domainType, Query query, @Nullable String collection) { ReactiveRemoveSupport(ReactiveMongoTemplate template, Class<S> domainType, Query query, @Nullable String collection,
QueryResultConverter<? super S, ? extends T> resultConverter) {
this.template = template; this.template = template;
this.domainType = domainType; this.domainType = domainType;
this.query = query; this.query = query;
this.collection = collection; this.collection = collection;
this.resultConverter = resultConverter;
} }
@Override @Override
@ -70,7 +73,7 @@ class ReactiveRemoveOperationSupport implements ReactiveRemoveOperation {
Assert.hasText(collection, "Collection must not be null nor empty"); Assert.hasText(collection, "Collection must not be null nor empty");
return new ReactiveRemoveSupport<>(template, domainType, query, collection); return new ReactiveRemoveSupport<>(template, domainType, query, collection, resultConverter);
} }
@Override @Override
@ -78,7 +81,7 @@ class ReactiveRemoveOperationSupport implements ReactiveRemoveOperation {
Assert.notNull(query, "Query must not be null"); Assert.notNull(query, "Query must not be null");
return new ReactiveRemoveSupport<>(template, domainType, query, collection); return new ReactiveRemoveSupport<>(template, domainType, query, collection, resultConverter);
} }
@Override @Override
@ -94,7 +97,13 @@ class ReactiveRemoveOperationSupport implements ReactiveRemoveOperation {
String collectionName = getCollectionName(); String collectionName = getCollectionName();
return template.doFindAndDelete(collectionName, query, domainType); return template.doFindAndDelete(collectionName, query, domainType, resultConverter);
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public <R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter) {
return new ReactiveRemoveSupport<>(template, (Class) domainType, query, collection, converter);
} }
private String getCollectionName() { private String getCollectionName() {

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

@ -15,6 +15,7 @@
*/ */
package org.springframework.data.mongodb.core; package org.springframework.data.mongodb.core;
import org.jetbrains.annotations.Contract;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
@ -64,6 +65,18 @@ public interface ReactiveUpdateOperation {
*/ */
interface TerminatingFindAndModify<T> { interface TerminatingFindAndModify<T> {
/**
* Map the query result to a different type using {@link QueryResultConverter}.
*
* @param <R> {@link Class type} of the result.
* @param converter the converter, must not be {@literal null}.
* @return new instance of {@link TerminatingFindAndModify}.
* @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
* @since 5.0
*/
@Contract("_ -> new")
<R> TerminatingFindAndModify<R> map(QueryResultConverter<? super T, ? extends R> converter);
/** /**
* Find, modify and return the first matching document. * Find, modify and return the first matching document.
* *
@ -97,6 +110,18 @@ public interface ReactiveUpdateOperation {
*/ */
interface TerminatingFindAndReplace<T> extends TerminatingReplace { interface TerminatingFindAndReplace<T> extends TerminatingReplace {
/**
* Map the query result to a different type using {@link QueryResultConverter}.
*
* @param <R> {@link Class type} of the result.
* @param converter the converter, must not be {@literal null}.
* @return new instance of {@link TerminatingFindAndModify}.
* @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
* @since 5.0
*/
@Contract("_ -> new")
<R> TerminatingFindAndReplace<R> map(QueryResultConverter<? super T, ? extends R> converter);
/** /**
* Find, replace and return the first matching document. * Find, replace and return the first matching document.
* *

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

@ -47,10 +47,10 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(domainType, "DomainType must not be null"); Assert.notNull(domainType, "DomainType must not be null");
return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType); return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType, QueryResultConverter.entity());
} }
static class ReactiveUpdateSupport<T> static class ReactiveUpdateSupport<S, T>
implements ReactiveUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T>, implements ReactiveUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T>,
FindAndReplaceWithOptions<T>, FindAndReplaceWithProjection<T>, TerminatingFindAndReplace<T> { FindAndReplaceWithOptions<T>, FindAndReplaceWithProjection<T>, TerminatingFindAndReplace<T> {
@ -62,11 +62,12 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
private final @Nullable FindAndModifyOptions findAndModifyOptions; private final @Nullable FindAndModifyOptions findAndModifyOptions;
private final @Nullable FindAndReplaceOptions findAndReplaceOptions; private final @Nullable FindAndReplaceOptions findAndReplaceOptions;
private final @Nullable Object replacement; private final @Nullable Object replacement;
private final Class<T> targetType; private final Class<S> targetType;
private final QueryResultConverter<? super S, ? extends T> resultConverter;
ReactiveUpdateSupport(ReactiveMongoTemplate template, Class<?> domainType, Query query, @Nullable UpdateDefinition update, ReactiveUpdateSupport(ReactiveMongoTemplate template, Class<?> domainType, Query query, @Nullable UpdateDefinition update,
@Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, @Nullable FindAndReplaceOptions findAndReplaceOptions, @Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, @Nullable FindAndReplaceOptions findAndReplaceOptions,
@Nullable Object replacement, Class<T> targetType) { @Nullable Object replacement, Class<S> targetType, QueryResultConverter<? super S, ? extends T> resultConverter) {
this.template = template; this.template = template;
this.domainType = domainType; this.domainType = domainType;
@ -77,6 +78,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
this.findAndReplaceOptions = findAndReplaceOptions; this.findAndReplaceOptions = findAndReplaceOptions;
this.replacement = replacement; this.replacement = replacement;
this.targetType = targetType; this.targetType = targetType;
this.resultConverter = resultConverter;
} }
@Override @Override
@ -85,7 +87,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(update, "Update must not be null"); Assert.notNull(update, "Update must not be null");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType); findAndReplaceOptions, replacement, targetType, resultConverter);
} }
@Override @Override
@ -94,7 +96,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.hasText(collection, "Collection must not be null nor empty"); Assert.hasText(collection, "Collection must not be null nor empty");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType); findAndReplaceOptions, replacement, targetType, resultConverter);
} }
@Override @Override
@ -108,14 +110,14 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
} }
@Override @Override
@SuppressWarnings("NullAway") @SuppressWarnings({"unchecked", "rawtypes", "NullAway"})
public Mono<T> findAndModify() { public Mono<T> findAndModify() {
String collectionName = getCollectionName(); String collectionName = getCollectionName();
return template.findAndModify(query, update, return template.findAndModify(query, update,
findAndModifyOptions != null ? findAndModifyOptions : FindAndModifyOptions.none(), targetType, findAndModifyOptions != null ? findAndModifyOptions : FindAndModifyOptions.none(), (Class) targetType,
collectionName); collectionName, resultConverter);
} }
@Override @Override
@ -126,7 +128,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
return template.findAndReplace(query, replacement, return template.findAndReplace(query, replacement,
findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.none(), (Class) domainType, findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.none(), (Class) domainType,
getCollectionName(), targetType); getCollectionName(), targetType, resultConverter);
} }
@Override @Override
@ -135,7 +137,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(query, "Query must not be null"); Assert.notNull(query, "Query must not be null");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType); findAndReplaceOptions, replacement, targetType, resultConverter);
} }
@Override @Override
@ -149,7 +151,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(options, "Options must not be null"); Assert.notNull(options, "Options must not be null");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options, return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options,
findAndReplaceOptions, replacement, targetType); findAndReplaceOptions, replacement, targetType, resultConverter);
} }
@Override @Override
@ -158,7 +160,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(replacement, "Replacement must not be null"); Assert.notNull(replacement, "Replacement must not be null");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType); findAndReplaceOptions, replacement, targetType, resultConverter);
} }
@Override @Override
@ -167,7 +169,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(options, "Options must not be null"); Assert.notNull(options, "Options must not be null");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, options, return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, options,
replacement, targetType); replacement, targetType, resultConverter);
} }
@Override @Override
@ -178,7 +180,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
target.upsert(); target.upsert();
} }
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
target, replacement, targetType); target, replacement, targetType, resultConverter);
} }
@Override @Override
@ -187,7 +189,13 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(resultType, "ResultType must not be null"); Assert.notNull(resultType, "ResultType must not be null");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, resultType); findAndReplaceOptions, replacement, resultType, QueryResultConverter.entity());
}
@Override
public <R> ReactiveUpdateSupport<S, R> map(QueryResultConverter<? super T, ? extends R> converter) {
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter));
} }
@Override @Override

16
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java

@ -15,13 +15,14 @@
*/ */
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 static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.*; import static org.springframework.data.mongodb.core.query.Query.query;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -108,6 +109,15 @@ class ReactiveRemoveOperationSupportTests {
.expectNext(han).verifyComplete(); .expectNext(han).verifyComplete();
} }
@Test // GH-4949
void removeConvertAndReturnAllMatching() {
template.remove(Person.class).matching(query(where("firstname").is("han"))).map((raw, it) -> Optional.of(it.get()))
.findAndRemove().as(StepVerifier::create).expectNext(Optional.of(han)).verifyComplete();
template.findById(han.id, Person.class).as(StepVerifier::create).verifyComplete();
}
@org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS)
static class Person { static class Person {

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

@ -15,13 +15,15 @@
*/ */
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 static org.springframework.data.mongodb.core.query.Criteria.*; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.data.mongodb.core.query.Query.*; import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import org.bson.BsonString; import org.bson.BsonString;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -175,6 +177,18 @@ class ReactiveUpdateOperationSupportTests {
"Han"); "Han");
} }
@Test // GH-4949
void findAndModifyWithWithResultConversion() {
template.update(Jedi.class).inCollection(STAR_WARS).matching(query(where("_id").is(han.getId())))
.apply(new Update().set("name", "Han")).map((raw, it) -> Optional.of(it.get())).findAndModify()
.as(StepVerifier::create).consumeNextWith(actual -> assertThat(actual.get().getName()).isEqualTo("han"))
.verifyComplete();
assertThat(blocking.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname",
"Han");
}
@Test // DATAMONGO-1719 @Test // DATAMONGO-1719
void findAndModifyWithOptions() { void findAndModifyWithOptions() {
@ -225,6 +239,18 @@ class ReactiveUpdateOperationSupportTests {
}).verifyComplete(); }).verifyComplete();
} }
@Test // GH-4949
void findAndReplaceWithResultConversion() {
Person luke = new Person();
luke.firstname = "Luke";
template.update(Person.class).matching(queryHan()).replaceWith(luke).map((raw, it) -> Optional.of(it.get())).findAndReplace() //
.as(StepVerifier::create).consumeNextWith(it -> {
assertThat(it.get().getFirstname()).isEqualTo(han.firstname);
}).verifyComplete();
}
@Test // DATAMONGO-1827 @Test // DATAMONGO-1827
void findAndReplaceWithCollection() { void findAndReplaceWithCollection() {

13
src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc

@ -116,6 +116,19 @@ WARNING: Projections must not be applied to xref:mongodb/mapping/document-refere
You can switch between retrieving a single entity and retrieving multiple entities as a `List` or a `Stream` through the terminating methods: `first()`, `one()`, `all()`, or `stream()`. You can switch between retrieving a single entity and retrieving multiple entities as a `List` or a `Stream` through the terminating methods: `first()`, `one()`, `all()`, or `stream()`.
Results can be contextually post-processed by using a `QueryResultConverter` that has access to both the raw result `Document` and the already mapped object by calling `map(...)` as outlined below.
[source,java]
====
----
List<Optional<Jedi>> result = template.query(Person.class)
.as(Jedi.class)
.matching(query(where("firstname").is("luke")))
.map((document, reader) -> Optional.of(reader.get()))
.all();
----
====
When writing a geo-spatial query with `near(NearQuery)`, the number of terminating methods is altered to include only the methods that are valid for running a `geoNear` command in MongoDB (fetching entities as a `GeoResult` within `GeoResults`), as the following example shows: When writing a geo-spatial query with `near(NearQuery)`, the number of terminating methods is altered to include only the methods that are valid for running a `geoNear` command in MongoDB (fetching entities as a `GeoResult` within `GeoResults`), as the following example shows:
[tabs] [tabs]

Loading…
Cancel
Save