From ec49c14bb999babfeb97fd15493ada7b2e5e2258 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 29 Apr 2025 13:32:25 +0200 Subject: [PATCH] Support QueryResultConverter for delete, replace and update operations. Original Pull Request: #4949 --- .../core/ExecutableRemoveOperation.java | 39 +++++--- .../ExecutableRemoveOperationSupport.java | 23 +++-- .../core/ExecutableUpdateOperation.java | 27 ++++++ .../ExecutableUpdateOperationSupport.java | 50 ++++++---- .../data/mongodb/core/MongoTemplate.java | 91 ++++++++++++++----- ...ExecutableRemoveOperationSupportTests.java | 9 ++ ...ExecutableUpdateOperationSupportTests.java | 24 +++++ .../mongodb/core/MongoTemplateUnitTests.java | 4 +- .../core/QueryResultConverterUnitTests.java | 89 ++++++++++++++++++ 9 files changed, 295 insertions(+), 61 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java index a10cd0317..9f4a0109e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java @@ -19,6 +19,7 @@ import java.util.List; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; import com.mongodb.client.result.DeleteResult; @@ -54,11 +55,36 @@ public interface ExecutableRemoveOperation { */ ExecutableRemove remove(Class domainType); + interface TerminatingResults { + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@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 x.y + */ + @Contract("_ -> new") + TerminatingResults map(QueryResultConverter converter); + + /** + * Remove and return all matching documents.
+ * NOTE: The entire list of documents will be fetched before sending the actual delete commands. + * Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete + * operation. + * + * @return empty {@link List} if no match found. Never {@literal null}. + */ + List findAndRemove(); + } + /** * @author Christoph Strobl * @since 2.0 */ - interface TerminatingRemove { + interface TerminatingRemove extends TerminatingResults { /** * Remove all documents matching. @@ -73,16 +99,6 @@ public interface ExecutableRemoveOperation { * @return the {@link DeleteResult}. Never {@literal null}. */ DeleteResult one(); - - /** - * Remove and return all matching documents.
- * NOTE: The entire list of documents will be fetched before sending the actual delete commands. - * Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete - * operation. - * - * @return empty {@link List} if no match found. Never {@literal null}. - */ - List findAndRemove(); } /** @@ -105,7 +121,6 @@ public interface ExecutableRemoveOperation { RemoveWithQuery inCollection(String collection); } - /** * @author Christoph Strobl * @since 2.0 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java index e53e80b10..77cd924e5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java @@ -48,26 +48,28 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableRemoveSupport<>(tempate, domainType, ALL_QUERY, null); + return new ExecutableRemoveSupport<>(tempate, domainType, ALL_QUERY, null, QueryResultConverter.entity()); } /** * @author Christoph Strobl * @since 2.0 */ - static class ExecutableRemoveSupport implements ExecutableRemove, RemoveWithCollection { + static class ExecutableRemoveSupport implements ExecutableRemove, RemoveWithCollection { private final MongoTemplate template; - private final Class domainType; + private final Class domainType; private final Query query; @Nullable private final String collection; + private final QueryResultConverter resultConverter; - public ExecutableRemoveSupport(MongoTemplate template, Class domainType, Query query, - @Nullable String collection) { + public ExecutableRemoveSupport(MongoTemplate template, Class domainType, Query query, + @Nullable String collection, QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; this.query = query; this.collection = collection; + this.resultConverter = resultConverter; } @Override @@ -76,7 +78,7 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation { Assert.hasText(collection, "Collection must not be null nor empty"); - return new ExecutableRemoveSupport<>(template, domainType, query, collection); + return new ExecutableRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override @@ -85,7 +87,7 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation { Assert.notNull(query, "Query must not be null"); - return new ExecutableRemoveSupport<>(template, domainType, query, collection); + return new ExecutableRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override @@ -103,7 +105,12 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation { String collectionName = getCollectionName(); - return template.doFindAndDelete(collectionName, query, domainType); + return template.doFindAndDelete(collectionName, query, domainType, resultConverter); + } + + @Override + public TerminatingResults map(QueryResultConverter converter) { + return new ExecutableRemoveSupport<>(template, (Class) domainType, query, collection, converter); } private String getCollectionName() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java index 69365459b..925a1af80 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java @@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core; import java.util.Optional; import org.jspecify.annotations.Nullable; +import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingResults; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; @@ -25,6 +26,7 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; import com.mongodb.client.result.UpdateResult; +import org.springframework.lang.Contract; /** * {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify / findAndReplace @@ -69,6 +71,19 @@ public interface ExecutableUpdateOperation { */ interface TerminatingFindAndModify { + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@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 x.y + */ + @Contract("_ -> new") + TerminatingFindAndModify map(QueryResultConverter converter); + /** * Find, modify and return the first matching document. * @@ -130,6 +145,18 @@ public interface ExecutableUpdateOperation { */ @Nullable T findAndReplaceValue(); + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@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 x.y + */ + @Contract("_ -> new") + TerminatingFindAndReplace mapResult(QueryResultConverter converter); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java index 75756c6f1..56bef3815 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java @@ -47,7 +47,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType); + return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType, QueryResultConverter.entity()); } /** @@ -55,23 +55,25 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { * @since 2.0 */ @SuppressWarnings("rawtypes") - static class ExecutableUpdateSupport + static class ExecutableUpdateSupport implements ExecutableUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate, FindAndReplaceWithOptions, TerminatingFindAndReplace, FindAndReplaceWithProjection { private final MongoTemplate template; - private final Class domainType; + private final Class domainType; private final Query query; @Nullable private final UpdateDefinition update; @Nullable private final String collection; @Nullable private final FindAndModifyOptions findAndModifyOptions; @Nullable private final FindAndReplaceOptions findAndReplaceOptions; @Nullable private final Object replacement; - private final Class targetType; + private final QueryResultConverter resultConverter; + private final Class targetType; - ExecutableUpdateSupport(MongoTemplate template, Class domainType, Query query, @Nullable UpdateDefinition update, + ExecutableUpdateSupport(MongoTemplate template, Class domainType, Query query, @Nullable UpdateDefinition update, @Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, - @Nullable FindAndReplaceOptions findAndReplaceOptions, @Nullable Object replacement, Class targetType) { + @Nullable FindAndReplaceOptions findAndReplaceOptions, @Nullable Object replacement, Class targetType, + QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; @@ -82,6 +84,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { this.findAndReplaceOptions = findAndReplaceOptions; this.replacement = replacement; this.targetType = targetType; + this.resultConverter = resultConverter; } @Override @@ -91,7 +94,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(update, "Update must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -101,7 +104,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.hasText(collection, "Collection must not be null nor empty"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -111,7 +114,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(options, "Options must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -121,7 +124,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(replacement, "Replacement must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -131,7 +134,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(options, "Options must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - options, replacement, targetType); + options, replacement, targetType, resultConverter); } @Override @@ -143,7 +146,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { target.upsert(); } return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - target, replacement, targetType); + target, replacement, targetType, resultConverter); } @Override @@ -153,7 +156,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(query, "Query must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -163,7 +166,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { Assert.notNull(resultType, "ResultType must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, resultType); + findAndReplaceOptions, replacement, resultType, QueryResultConverter.entity()); } @Override @@ -181,13 +184,26 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { return doUpdate(true, true); } + @Override + public TerminatingFindAndModify map(QueryResultConverter converter) { + + return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, + findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); + } + + @Override + public TerminatingFindAndReplace mapResult(QueryResultConverter converter) { + return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, + findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); + } + @Override @SuppressWarnings("NullAway") public @Nullable T findAndModifyValue() { return template.findAndModify(query, update, findAndModifyOptions != null ? findAndModifyOptions : new FindAndModifyOptions(), targetType, - getCollectionName()); + getCollectionName(), resultConverter); } @Override @@ -195,8 +211,8 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { public @Nullable T findAndReplaceValue() { return (T) template.findAndReplace(query, replacement, - findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), domainType, - getCollectionName(), targetType); + findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), (Class) domainType, + getCollectionName(), targetType, (QueryResultConverter) resultConverter); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index edf8a069a..5c9f38282 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -122,6 +122,7 @@ import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.mapreduce.MapReduceResults; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; @@ -1143,7 +1144,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable @Override public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, - Class entityClass, String collectionName) { + Class entityClass, String collectionName) { + return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity()); + } + + + T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName, QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(update, "Update must not be null"); @@ -1163,12 +1170,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } return doFindAndModify(createDelegate(query), collectionName, query.getQueryObject(), query.getFieldsObject(), - getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); + getMappedSortObject(query, entityClass), entityClass, update, optionsToUse, resultConverter); } @Override public @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, - Class entityType, String collectionName, Class resultType) { + Class entityType, String collectionName, Class resultType) { + return findAndReplace(query, replacement, options, entityType, collectionName, resultType, QueryResultConverter.entity()); + } + + public @Nullable R findAndReplace(Query query, S replacement, FindAndReplaceOptions options, + Class entityType, String collectionName, Class resultType, QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(replacement, "Replacement must not be null"); @@ -1195,8 +1207,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName)); maybeCallBeforeSave(replacement, mappedReplacement, collectionName); - T saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, - queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection); + + R saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, + queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection, resultConverter); if (saved != null) { maybeEmitEvent(new AfterSaveEvent<>(saved, mappedReplacement, collectionName)); @@ -2187,17 +2200,48 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ @SuppressWarnings("NullAway") protected List doFindAndDelete(String collectionName, Query query, Class entityClass) { + return doFindAndDelete(collectionName, query, entityClass, QueryResultConverter.entity()); + } + + protected List doFindAndDelete(String collectionName, Query query, Class entityClass, QueryResultConverter resultConverter) { + + List ids = new ArrayList<>(); + + - List result = find(query, entityClass, collectionName); +// QueryResultConverter tmpConverter = new QueryResultConverter() { +// @Override +// public S mapDocument(Document document, ConversionResultSupplier reader) { +// ids.add(document.get("_id")); +// return reader.get(); +// } +// }.andThen(resultConverter); + +// DocumentCallback callback = getResultReader(EntityProjection.nonProjecting(entityClass), collectionName, tmpConverter); + + QueryResultConverterCallback callback = new QueryResultConverterCallback(resultConverter, new ProjectingReadCallback(getConverter(), EntityProjection.nonProjecting(entityClass), collectionName)) { + @Override + public Object doWith(Document object) { + ids.add(object.get("_id")); + return super.doWith(object); + } + }; + + List result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), entityClass, + new QueryCursorPreparer(query, entityClass), callback); if (!CollectionUtils.isEmpty(result)) { - Query byIdInQuery = operations.getByIdInQuery(result); + 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()) { - byIdInQuery.withReadPreference(query.getReadPreference()); + removeQuery.withReadPreference(query.getReadPreference()); } - remove(byIdInQuery, entityClass, collectionName); + remove(removeQuery, entityClass, collectionName); } return result; @@ -2819,9 +2863,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } @SuppressWarnings("ConstantConditions") - protected @Nullable T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, - Document query, @Nullable Document fields, @Nullable Document sort, Class entityClass, UpdateDefinition update, - @Nullable FindAndModifyOptions options) { + protected @Nullable T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, + Document query, @Nullable Document fields, @Nullable Document sort, Class entityClass, UpdateDefinition update, + @Nullable FindAndModifyOptions options, QueryResultConverter resultConverter) { if (options == null) { options = new FindAndModifyOptions(); @@ -2843,10 +2887,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, serializeToJsonSafely(mappedUpdate), collectionName)); } + DocumentCallback callback = getResultReader(EntityProjection.nonProjecting(entityClass), collectionName, resultConverter); + return executeFindOneInternal( new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate, update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), - new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); + callback, collectionName); } /** @@ -2865,15 +2911,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. */ @Nullable - protected T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, + protected T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, - com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, + com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { - EntityProjection projection = operations.introspectProjection(resultType, entityType); + EntityProjection projection = operations.introspectProjection(resultType, entityType); return doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, collation, - entityType, replacement, options, projection); + entityType, replacement, options, projection, QueryResultConverter.entity()); } CollectionPreparerDelegate createDelegate(Query query) { @@ -2908,10 +2954,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @since 3.4 */ @Nullable - private T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, + private R doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, - com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, - FindAndReplaceOptions options, EntityProjection projection) { + com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, EntityProjection projection, QueryResultConverter resultConverter) { if (LOGGER.isDebugEnabled()) { LOGGER @@ -2922,8 +2968,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, serializeToJsonSafely(mappedSort), entityType, serializeToJsonSafely(replacement), collectionName)); } + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields, mappedSort, - replacement, collation, options), new ProjectingReadCallback<>(mongoConverter, projection, collectionName), + replacement, collation, options),callback, collectionName); } @@ -3421,7 +3468,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } } - static final class QueryResultConverterCallback implements DocumentCallback { + static class QueryResultConverterCallback implements DocumentCallback { private final QueryResultConverter converter; private final DocumentCallback delegate; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java index 621e2a076..54a477300 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java @@ -21,6 +21,7 @@ import static org.springframework.data.mongodb.core.query.Query.*; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -108,6 +109,14 @@ class ExecutableRemoveOperationSupportTests { assertThat(result).containsExactly(han); } + @Test // GH-0 + void removeAndReturnAllMatchingWithResultConverter() { + + List> result = template.remove(Person.class).matching(query(where("firstname").is("han"))).map((raw, converted) -> Optional.of(converted.get())).findAndRemove(); + + assertThat(result).containsExactly(Optional.of(han)); + } + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) static class Person { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java index e7f50dab5..86c4693c3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java @@ -185,6 +185,17 @@ class ExecutableUpdateOperationSupportTests { assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); } + @Test // GH- + void findAndModifyWithResultConverter() { + + Optional result = template.update(Person.class).matching(queryHan()) + .apply(new Update().set("firstname", "Han")).withOptions(FindAndModifyOptions.options().returnNew(true)) + .map((raw, converted) -> Optional.of(converted.get())) + .findAndModifyValue(); + + assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); + } + @Test // DATAMONGO-1563 void upsert() { @@ -282,6 +293,19 @@ class ExecutableUpdateOperationSupportTests { assertThat(result.getName()).isEqualTo(han.firstname); } + @Test // GH- + void findAndReplaceWithResultConverter() { + + Person luke = new Person(); + luke.firstname = "Luke"; + + Optional result = template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class) // + .mapResult((raw, converted) -> Optional.of(converted.get())) + .findAndReplaceValue(); + + assertThat(result.get()).isInstanceOf(Jedi.class).extracting(Jedi::getName).isEqualTo(han.firstname); + } + private Query queryHan() { return query(where("id").is(han.getId())); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 40d86fce5..ef72548fa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -437,8 +437,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { verify(collection, times(1)).deleteMany(queryCaptor.capture(), any()); - Document idField = DocumentTestUtils.getAsDocument(queryCaptor.getValue(), "_id"); - assertThat((List) idField.get("$in")).containsExactly(Integer.valueOf(0), Integer.valueOf(1)); + List ors = DocumentTestUtils.getAsDBList(queryCaptor.getValue(), "$or"); + assertThat(ors).containsExactlyInAnyOrder(new Document("_id", 0), new Document("_id", 1)); } @Test // DATAMONGO-566 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java new file mode 100644 index 000000000..c94a9fab0 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.QueryResultConverter.ConversionResultSupplier; + +/** + * @author Christoph Strobl + */ +class QueryResultConverterUnitTests { + + public static final ConversionResultSupplier ERROR_SUPPLIER = () -> { + throw new IllegalStateException("must not read conversion result"); + }; + + @Test // GH- + void converterDoesNotEagerlyRetrieveConversionResultFromSupplier() { + + QueryResultConverter converter = new QueryResultConverter() { + + @Override + public String mapDocument(Document document, ConversionResultSupplier reader) { + return "done"; + } + }; + + assertThat(converter.mapDocument(new Document(), ERROR_SUPPLIER)).isEqualTo("done"); + } + + @Test // GH- + void converterPassesOnConversionResultToNextStage() { + + Document source = new Document("value", "10"); + + QueryResultConverter stagedConverter = new QueryResultConverter() { + + @Override + public String mapDocument(Document document, ConversionResultSupplier reader) { + return document.get("value", "-1"); + } + }.andThen(new QueryResultConverter() { + + @Override + public Integer mapDocument(Document document, ConversionResultSupplier reader) { + + assertThat(document).isEqualTo(source); + return Integer.valueOf(reader.get()); + } + }); + + assertThat(stagedConverter.mapDocument(source, ERROR_SUPPLIER)).isEqualTo(10); + } + + @Test // GH- + void entityConverterDelaysConversion() { + + Document source = new Document("value", "10"); + + QueryResultConverter converter = QueryResultConverter. entity() + .andThen(new QueryResultConverter() { + + @Override + public Integer mapDocument(Document document, ConversionResultSupplier reader) { + + assertThat(document).isEqualTo(source); + return Integer.valueOf(document.get("value", "20")); + } + }); + + assertThat(converter.mapDocument(source, ERROR_SUPPLIER)).isEqualTo(10); + } +}