From a7e6b26796fd1dae76101bf5e26e4e36256bb9fc Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 29 May 2019 08:30:07 +0200 Subject: [PATCH] DATAMONGO-2261 - Adapt to changes in DATACMNS-1467. Use the reworked version of the EntityCallback method lookup. Also fix issues with callbacks not invoked when intended and rework the reactive flow by removing deeply nested constructs. Update documentation and add EntityCallbacks to BulkOperations. Original Pull Request: #742 --- .../mongodb/core/DefaultBulkOperations.java | 69 ++++- .../data/mongodb/core/MongoTemplate.java | 52 ++-- .../mongodb/core/ReactiveMongoTemplate.java | 149 +++++++--- .../mapping/event/BeforeConvertCallback.java | 4 +- .../mapping/event/BeforeSaveCallback.java | 5 +- .../event/ReactiveBeforeSaveCallback.java | 2 +- .../config/AuditingIntegrationTests.java | 11 +- ...DefaultBulkOperationsIntegrationTests.java | 8 +- .../core/DefaultBulkOperationsUnitTests.java | 58 +++- .../mongodb/core/MongoTemplateUnitTests.java | 259 +++++++++++++++++- .../core/ReactiveMongoTemplateUnitTests.java | 175 +++++++++++- .../reference/mongo-entity-callbacks.adoc | 30 ++ src/main/asciidoc/reference/mongodb.adoc | 5 + 13 files changed, 736 insertions(+), 91 deletions(-) create mode 100644 src/main/asciidoc/reference/mongo-entity-callbacks.adoc diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java index 93a281e00..30a1ce08b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java @@ -27,9 +27,12 @@ import java.util.stream.Collectors; import org.bson.Document; import org.bson.conversions.Bson; import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; @@ -55,7 +58,7 @@ class DefaultBulkOperations implements BulkOperations { private final MongoOperations mongoOperations; private final String collectionName; private final BulkOperationContext bulkOperationContext; - private final List> models = new ArrayList<>(); + private final List models = new ArrayList<>(); private PersistenceExceptionTranslator exceptionTranslator; private @Nullable WriteConcern defaultWriteConcern; @@ -112,7 +115,8 @@ class DefaultBulkOperations implements BulkOperations { Assert.notNull(document, "Document must not be null!"); - models.add(new InsertOneModel<>(getMappedObject(document))); + Object source = maybeInvokeBeforeConvertCallback(document); + addModel(source, new InsertOneModel<>(getMappedObject(source))); return this; } @@ -226,7 +230,7 @@ class DefaultBulkOperations implements BulkOperations { DeleteOptions deleteOptions = new DeleteOptions(); query.getCollation().map(Collation::toMongoCollation).ifPresent(deleteOptions::collation); - models.add(new DeleteManyModel<>(query.getQueryObject(), deleteOptions)); + addModel(query, new DeleteManyModel<>(query.getQueryObject(), deleteOptions)); return this; } @@ -262,8 +266,9 @@ class DefaultBulkOperations implements BulkOperations { replaceOptions.upsert(options.isUpsert()); query.getCollation().map(Collation::toMongoCollation).ifPresent(replaceOptions::collation); - models.add( - new ReplaceOneModel<>(getMappedQuery(query.getQueryObject()), getMappedObject(replacement), replaceOptions)); + Object source = maybeInvokeBeforeConvertCallback(replacement); + addModel(source, + new ReplaceOneModel<>(getMappedQuery(query.getQueryObject()), getMappedObject(source), replaceOptions)); return this; } @@ -278,7 +283,17 @@ class DefaultBulkOperations implements BulkOperations { try { return mongoOperations.execute(collectionName, collection -> { - return collection.bulkWrite(models.stream().map(this::mapWriteModel).collect(Collectors.toList()), bulkOptions); + return collection.bulkWrite(models.stream().map(it -> { + + if (it.getModel() instanceof InsertOneModel) { + maybeInvokeBeforeSaveCallback(it.getSource(), ((InsertOneModel) it.getModel()).getDocument()); + } + if (it.getModel() instanceof ReplaceOneModel) { + maybeInvokeBeforeSaveCallback(it.getSource(), ((ReplaceOneModel) it.getModel()).getReplacement()); + } + + return mapWriteModel(it.getModel()); + }).collect(Collectors.toList()), bulkOptions); }); } finally { this.bulkOptions = getBulkWriteOptions(bulkOperationContext.getBulkMode()); @@ -304,9 +319,9 @@ class DefaultBulkOperations implements BulkOperations { query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation); if (multi) { - models.add(new UpdateManyModel<>(query.getQueryObject(), update.getUpdateObject(), options)); + addModel(update, new UpdateManyModel<>(query.getQueryObject(), update.getUpdateObject(), options)); } else { - models.add(new UpdateOneModel<>(query.getQueryObject(), update.getUpdateObject(), options)); + addModel(update, new UpdateOneModel<>(query.getQueryObject(), update.getUpdateObject(), options)); } return this; @@ -362,10 +377,34 @@ class DefaultBulkOperations implements BulkOperations { } Document sink = new Document(); + mongoOperations.getConverter().write(source, sink); return sink; } + private void addModel(Object source, WriteModel model) { + models.add(new SourceAwareWriteModelHolder(source, model)); + } + + private Object maybeInvokeBeforeConvertCallback(Object value) { + + if (bulkOperationContext.getEntityCallbacks() == null) { + return value; + } + + return bulkOperationContext.getEntityCallbacks().callback(BeforeConvertCallback.class, value, collectionName); + } + + private Object maybeInvokeBeforeSaveCallback(Object value, Document mappedDocument) { + + if (bulkOperationContext.getEntityCallbacks() == null) { + return value; + } + + return bulkOperationContext.getEntityCallbacks().callback(BeforeSaveCallback.class, value, mappedDocument, + collectionName); + } + private static BulkWriteOptions getBulkWriteOptions(BulkMode bulkMode) { BulkWriteOptions options = new BulkWriteOptions(); @@ -395,5 +434,19 @@ class DefaultBulkOperations implements BulkOperations { @NonNull Optional> entity; @NonNull QueryMapper queryMapper; @NonNull UpdateMapper updateMapper; + EntityCallbacks entityCallbacks; + } + + /** + * Value object chaining together an actual source with its {@link WriteModel} representation. + * + * @since 2.2 + * @author Christoph Strobl + */ + @Value + private static class SourceAwareWriteModelHolder { + + Object source; + WriteModel model; } } 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 13d5d62fa..eba273b9f 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 @@ -35,7 +35,6 @@ import org.bson.codecs.Codec; import org.bson.conversions.Bson; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -56,7 +55,7 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; -import org.springframework.data.mapping.callback.SimpleEntityCallbacks; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.MongoDbFactory; @@ -143,17 +142,7 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.CountOptions; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.DeleteOptions; -import com.mongodb.client.model.FindOneAndDeleteOptions; -import com.mongodb.client.model.FindOneAndReplaceOptions; -import com.mongodb.client.model.FindOneAndUpdateOptions; -import com.mongodb.client.model.ReplaceOptions; -import com.mongodb.client.model.ReturnDocument; -import com.mongodb.client.model.UpdateOptions; -import com.mongodb.client.model.ValidationAction; -import com.mongodb.client.model.ValidationLevel; +import com.mongodb.client.model.*; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; @@ -214,7 +203,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private WriteResultChecking writeResultChecking = WriteResultChecking.NONE; private @Nullable ReadPreference readPreference; private @Nullable ApplicationEventPublisher eventPublisher; - private @Nullable SimpleEntityCallbacks entityCallbacks; + private @Nullable EntityCallbacks entityCallbacks; private @Nullable ResourceLoader resourceLoader; private @Nullable MongoPersistentEntityIndexCreator indexCreator; @@ -360,7 +349,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, eventPublisher = applicationContext; - entityCallbacks = new SimpleEntityCallbacks(applicationContext); + if (entityCallbacks == null) { + setEntityCallbacks(EntityCallbacks.create(applicationContext)); + } if (mappingContext instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); @@ -372,6 +363,22 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); } + /** + * Set the {@link EntityCallbacks} instance to use when invoking + * {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the {@link BeforeSaveCallback}. + *

+ * Overrides potentially existing {@link EntityCallbacks}. + * + * @param entityCallbacks must not be {@literal null}. + * @throws IllegalArgumentException if the given instance is {@literal null}. + * @since 2.2 + */ + public void setEntityCallbacks(EntityCallbacks entityCallbacks) { + + Assert.notNull(entityCallbacks, "EntityCallbacks must not be null!"); + this.entityCallbacks = entityCallbacks; + } + /** * Inspects the given {@link ApplicationContext} for {@link MongoPersistentEntityIndexCreator} and those in turn if * they were registered for the current {@link MappingContext}. If no creator for the current {@link MappingContext} @@ -771,7 +778,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.hasText(collectionName, "Collection name must not be null or empty!"); DefaultBulkOperations operations = new DefaultBulkOperations(this, collectionName, new BulkOperationContext(mode, - Optional.ofNullable(getPersistentEntity(entityType)), queryMapper, updateMapper)); + Optional.ofNullable(getPersistentEntity(entityType)), queryMapper, updateMapper, entityCallbacks)); operations.setExceptionTranslator(exceptionTranslator); operations.setDefaultWriteConcern(writeConcern); @@ -1098,8 +1105,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), entity); Document mappedSort = queryMapper.getMappedSort(query.getSortObject(), entity); + replacement = maybeCallBeforeConvert(replacement, collectionName); Document mappedReplacement = operations.forEntity(replacement).toMappedDocument(this.mongoConverter).getDocument(); + maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName)); + maybeCallBeforeSave(replacement, mappedReplacement, collectionName); + return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, operations.forType(entityType).getCollation(query).map(Collation::toMongoCollation).orElse(null), entityType, mappedReplacement, options, resultType); @@ -2340,8 +2351,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, protected T maybeCallBeforeConvert(T object, String collection) { if (null != entityCallbacks) { - return (T) entityCallbacks.callback(object, BeforeConvertCallback.class, - (cb, t) -> cb.onBeforeConvert(t, collection)); + return entityCallbacks.callback(BeforeConvertCallback.class, object, collection); } return object; @@ -2351,8 +2361,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, protected T maybeCallBeforeSave(T object, Document document, String collection) { if (null != entityCallbacks) { - return (T) entityCallbacks.callback(object, BeforeSaveCallback.class, - (cb, t) -> cb.onBeforeSave(t, document, collection)); + return entityCallbacks.callback(BeforeSaveCallback.class, object, document, collection); } return object; @@ -2679,9 +2688,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, entityType, serializeToJsonSafely(replacement), collectionName); } - maybeEmitEvent(new BeforeSaveEvent<>(replacement, replacement, collectionName)); - replacement = maybeCallBeforeSave(replacement, replacement, collectionName); - return executeFindOneInternal( new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), new ProjectingReadCallback<>(mongoConverter, entityType, resultType, collectionName), collectionName); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 7495793f9..ec0d5b663 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -20,7 +20,6 @@ import static org.springframework.data.mongodb.core.query.SerializationUtils.*; import lombok.AccessLevel; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.util.NumberUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -115,6 +114,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -358,7 +358,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati prepareIndexCreator(applicationContext); eventPublisher = applicationContext; - entityCallbacks = new ReactiveEntityCallbacks(applicationContext); + + if (entityCallbacks == null) { + setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext)); + } + if (mappingContext instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); } @@ -367,6 +371,23 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); } + /** + * Set the {@link ReactiveEntityCallbacks} instance to use when invoking + * {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the + * {@link ReactiveBeforeSaveCallback}. + *

+ * Overrides potentially existing {@link ReactiveEntityCallbacks}. + * + * @param entityCallbacks must not be {@literal null}. + * @throws IllegalArgumentException if the given instance is {@literal null}. + * @since 2.2 + */ + public void setEntityCallbacks(ReactiveEntityCallbacks entityCallbacks) { + + Assert.notNull(entityCallbacks, "EntityCallbacks must not be null!"); + this.entityCallbacks = entityCallbacks; + } + /** * Inspects the given {@link ApplicationContext} for {@link ReactiveMongoPersistentEntityIndexCreator} and those in * turn if they were registered for the current {@link MappingContext}. If no creator for the current @@ -1166,11 +1187,25 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), entity); Document mappedSort = queryMapper.getMappedSort(query.getSortObject(), entity); - Document mappedReplacement = operations.forEntity(replacement).toMappedDocument(this.mongoConverter).getDocument(); - - return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, - operations.forType(entityType).getCollation(query).map(Collation::toMongoCollation).orElse(null), entityType, - mappedReplacement, options, resultType); + return Mono.just(PersistableEntityModel.of(replacement, collectionName)) // + .doOnNext(it -> maybeEmitEvent(new BeforeConvertEvent<>(it.getSource(), it.getCollection()))) // + .flatMap(it -> maybeCallBeforeConvert(it.getSource(), it.getCollection()).map(it::mutate)) + .map(it -> it + .addTargetDocument(operations.forEntity(it.getSource()).toMappedDocument(mongoConverter).getDocument())) // + .doOnNext(it -> maybeEmitEvent(new BeforeSaveEvent(it.getSource(), it.getTarget(), it.getCollection()))) // + .flatMap(it -> { + + PersistableEntityModel flowObject = (PersistableEntityModel) it; + return maybeCallBeforeSave(flowObject.getSource(), flowObject.getTarget(), flowObject.getCollection()) + .map(potentiallyModified -> PersistableEntityModel.of(potentiallyModified, flowObject.getTarget(), + flowObject.getCollection())); + }).flatMap(it -> { + + PersistableEntityModel flowObject = (PersistableEntityModel) it; + return doFindAndReplace(flowObject.getCollection(), mappedQuery, mappedFields, mappedSort, + operations.forType(entityType).getCollation(query).map(Collation::toMongoCollation).orElse(null), + entityType, flowObject.getTarget(), options, resultType); + }); } /* @@ -1321,32 +1356,31 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati protected Mono doInsert(String collectionName, T objectToSave, MongoWriter writer) { - return Mono.defer(() -> { + return Mono.just(PersistableEntityModel.of(objectToSave, collectionName)) // + .doOnNext(it -> maybeEmitEvent(new BeforeConvertEvent<>(it.getSource(), it.getCollection()))) // + .flatMap(it -> maybeCallBeforeConvert(it.getSource(), it.getCollection()).map(it::mutate)) // + .map(it -> { - BeforeConvertEvent event = new BeforeConvertEvent<>(objectToSave, collectionName); - T toConvert = maybeEmitEvent(event).getSource(); - return maybeCallBeforeConvert(toConvert, collectionName).flatMap(toSave -> { + AdaptibleEntity entity = operations.forEntity(it.getSource(), mongoConverter.getConversionService()); + entity.assertUpdateableIdIfNotSet(); - AdaptibleEntity entity = operations.forEntity(toConvert, mongoConverter.getConversionService()); - entity.assertUpdateableIdIfNotSet(); + return PersistableEntityModel.of(entity.initializeVersionProperty(), + entity.toMappedDocument(writer).getDocument(), it.getCollection()); + }).doOnNext(it -> maybeEmitEvent(new BeforeSaveEvent<>(it.getSource(), it.getTarget(), it.getCollection()))) // + .flatMap(it -> { - T initialized = entity.initializeVersionProperty(); - Document dbDoc = entity.toMappedDocument(writer).getDocument(); + return maybeCallBeforeSave(it.getSource(), it.getTarget(), it.getCollection()).map(it::mutate); - maybeEmitEvent(new BeforeSaveEvent<>(initialized, dbDoc, collectionName)); - return maybeCallBeforeSave(initialized, dbDoc, collectionName).flatMap(it -> { + }).flatMap(it -> { - Mono afterInsert = insertDocument(collectionName, dbDoc, it.getClass()).map(id -> { + return insertDocument(it.getCollection(), it.getTarget(), it.getSource().getClass()).map(id -> { - T saved = entity.populateIdIfNecessary(id); - maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); + T saved = operations.forEntity(it.getSource(), mongoConverter.getConversionService()) + .populateIdIfNecessary(id); + maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), collectionName)); return saved; }); - - return afterInsert; }); - }); - }); } /* @@ -2514,15 +2548,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati serializeToJsonSafely(replacement), collectionName); } - maybeEmitEvent(new BeforeSaveEvent<>(replacement, replacement, collectionName)); - - return maybeCallBeforeSave(replacement, replacement, collectionName).flatMap(it -> { - - return executeFindOneInternal( - new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, it, collation, options), - new ProjectingReadCallback<>(this.mongoConverter, entityType, resultType, collectionName), collectionName); + return executeFindOneInternal( + new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), + new ProjectingReadCallback<>(this.mongoConverter, entityType, resultType, collectionName), collectionName); - }); }); } @@ -2539,8 +2568,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati protected Mono maybeCallBeforeConvert(T object, String collection) { if (null != entityCallbacks) { - return entityCallbacks.callbackLater(object, ReactiveBeforeConvertCallback.class, - (cb, t) -> cb.onBeforeConvert(t, collection)); + return entityCallbacks.callback(ReactiveBeforeConvertCallback.class, object, collection); } return Mono.just(object); @@ -2550,8 +2578,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati protected Mono maybeCallBeforeSave(T object, Document document, String collection) { if (null != entityCallbacks) { - return entityCallbacks.callbackLater(object, ReactiveBeforeSaveCallback.class, - (cb, t) -> cb.onBeforeSave(t, document, collection)); + return entityCallbacks.callback(ReactiveBeforeSaveCallback.class, object, document, collection); } return Mono.just(object); @@ -3307,4 +3334,54 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } } + /** + * Value object chaining together a given source document with its mapped representation and the collection to persist + * it to. + * + * @param + * @author Christoph Strobl + * @since 2.2 + */ + private static class PersistableEntityModel { + + private final T source; + private final @Nullable Document target; + private final String collection; + + private PersistableEntityModel(T source, @Nullable Document target, String collection) { + + this.source = source; + this.target = target; + this.collection = collection; + } + + static PersistableEntityModel of(T source, String collection) { + return new PersistableEntityModel<>(source, null, collection); + } + + static PersistableEntityModel of(T source, Document target, String collection) { + return new PersistableEntityModel<>(source, target, collection); + } + + PersistableEntityModel mutate(T source) { + return new PersistableEntityModel(source, target, collection); + } + + PersistableEntityModel addTargetDocument(Document target) { + return new PersistableEntityModel(source, target, collection); + } + + T getSource() { + return source; + } + + @Nullable + Document getTarget() { + return target; + } + + String getCollection() { + return collection; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeConvertCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeConvertCallback.java index 2f39e1e6f..c942d1c30 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeConvertCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeConvertCallback.java @@ -16,20 +16,18 @@ package org.springframework.data.mongodb.core.mapping.event; import org.springframework.data.mapping.callback.EntityCallback; -import org.springframework.data.mapping.callback.SimpleEntityCallbacks; /** * Callback being invoked before a domain object is converted to be persisted. * * @author Mark Paluch * @since 2.2 - * @see SimpleEntityCallbacks */ @FunctionalInterface public interface BeforeConvertCallback extends EntityCallback { /** - * Entity callback method invoked before a domain object is converted to be persisted. Can return either the same of a + * Entity callback method invoked before a domain object is converted to be persisted. Can return either the same or a * modified instance of the domain object. * * @param entity the domain object to save. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeSaveCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeSaveCallback.java index e682e89eb..4a188daef 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeSaveCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeSaveCallback.java @@ -16,22 +16,19 @@ package org.springframework.data.mongodb.core.mapping.event; import org.bson.Document; - import org.springframework.data.mapping.callback.EntityCallback; -import org.springframework.data.mapping.callback.SimpleEntityCallbacks; /** * Entity callback triggered before save of a document. * * @author Mark Paluch * @since 2.2 - * @see SimpleEntityCallbacks */ @FunctionalInterface public interface BeforeSaveCallback extends EntityCallback { /** - * Entity callback method invoked before a domain object is saved. Can return either the same of a modified instance + * Entity callback method invoked before a domain object is saved. Can return either the same or a modified instance * of the domain object and can modify {@link Document} contents. This method called after converting the * {@code entity} to {@link Document} so effectively the document is used as outcome of invoking this callback. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeSaveCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeSaveCallback.java index bb085d6ff..fbd37c866 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeSaveCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeSaveCallback.java @@ -32,7 +32,7 @@ import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; public interface ReactiveBeforeSaveCallback extends EntityCallback { /** - * Entity callback method invoked before a domain object is saved. Can return either the same of a modified instance + * Entity callback method invoked before a domain object is saved. Can return either the same or a modified instance * of the domain object and can modify {@link Document} contents. This method is called after converting the * {@code entity} to {@link Document} so effectively the document is used as outcome of invoking this callback. * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java index b3bc81e24..c9a9787cb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java @@ -20,13 +20,12 @@ import static org.junit.Assert.*; import org.joda.time.DateTime; import org.junit.Test; - import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.mapping.callback.SimpleEntityCallbacks; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; @@ -38,7 +37,7 @@ import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback */ public class AuditingIntegrationTests { - @Test // DATAMONGO-577, DATAMONGO-800, DATAMONGO-883, 2261 + @Test // DATAMONGO-577, DATAMONGO-800, DATAMONGO-883, DATAMONGO-2261 public void enablesAuditingAndSetsPropertiesAccordingly() throws Exception { AbstractApplicationContext context = new ClassPathXmlApplicationContext("auditing.xml", getClass()); @@ -46,10 +45,10 @@ public class AuditingIntegrationTests { MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class); mappingContext.getPersistentEntity(Entity.class); - SimpleEntityCallbacks callbacks = new SimpleEntityCallbacks(context); + EntityCallbacks callbacks = EntityCallbacks.create(context); Entity entity = new Entity(); - entity = callbacks.callback(entity, BeforeConvertCallback.class, (cb, e) -> cb.onBeforeConvert(e, "collection-1")); + entity = callbacks.callback(BeforeConvertCallback.class, entity, "collection-1"); assertThat(entity.created, is(notNullValue())); assertThat(entity.modified, is(entity.created)); @@ -57,7 +56,7 @@ public class AuditingIntegrationTests { Thread.sleep(10); entity.id = 1L; - entity = callbacks.callback(entity, BeforeConvertCallback.class, (cb, e) -> cb.onBeforeConvert(e, "collection-1")); + entity = callbacks.callback(BeforeConvertCallback.class, entity, "collection-1"); assertThat(entity.created, is(notNullValue())); assertThat(entity.modified, is(not(entity.created))); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java index 2ec2eef7d..a252291c4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsIntegrationTests.java @@ -72,19 +72,19 @@ public class DefaultBulkOperationsIntegrationTests { @Test(expected = IllegalArgumentException.class) // DATAMONGO-934 public void rejectsNullMongoOperations() { new DefaultBulkOperations(null, COLLECTION_NAME, - new BulkOperationContext(BulkMode.ORDERED, Optional.empty(), null, null)); + new BulkOperationContext(BulkMode.ORDERED, Optional.empty(), null, null, null)); } @Test(expected = IllegalArgumentException.class) // DATAMONGO-934 public void rejectsNullCollectionName() { new DefaultBulkOperations(operations, null, - new BulkOperationContext(BulkMode.ORDERED, Optional.empty(), null, null)); + new BulkOperationContext(BulkMode.ORDERED, Optional.empty(), null, null, null)); } @Test(expected = IllegalArgumentException.class) // DATAMONGO-934 public void rejectsEmptyCollectionName() { - new DefaultBulkOperations(operations, "", new BulkOperationContext(BulkMode.ORDERED, Optional.empty(), null, null)); + new DefaultBulkOperations(operations, "", new BulkOperationContext(BulkMode.ORDERED, Optional.empty(), null, null, null)); } @Test // DATAMONGO-934 @@ -341,7 +341,7 @@ public class DefaultBulkOperationsIntegrationTests { : Optional.empty(); BulkOperationContext bulkOperationContext = new BulkOperationContext(mode, entity, - new QueryMapper(operations.getConverter()), new UpdateMapper(operations.getConverter())); + new QueryMapper(operations.getConverter()), new UpdateMapper(operations.getConverter()), null); DefaultBulkOperations bulkOps = new DefaultBulkOperations(operations, COLLECTION_NAME, bulkOperationContext); bulkOps.setDefaultWriteConcern(WriteConcern.ACKNOWLEDGED); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java index acdafcfb1..9115e3d09 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java @@ -26,7 +26,6 @@ import static org.springframework.data.mongodb.core.query.Query.*; import java.util.List; import java.util.Optional; -import com.mongodb.client.model.*; import org.bson.Document; import org.junit.Before; import org.junit.Test; @@ -36,6 +35,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext; @@ -46,12 +46,20 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Update; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.DeleteManyModel; +import com.mongodb.client.model.InsertOneModel; +import com.mongodb.client.model.ReplaceOneModel; +import com.mongodb.client.model.UpdateManyModel; +import com.mongodb.client.model.UpdateOneModel; +import com.mongodb.client.model.WriteModel; /** * Unit tests for {@link DefaultBulkOperations}. @@ -90,7 +98,7 @@ public class DefaultBulkOperationsUnitTests { ops = new DefaultBulkOperations(template, "collection-1", new BulkOperationContext(BulkMode.ORDERED, Optional.of(mappingContext.getPersistentEntity(SomeDomainType.class)), new QueryMapper(converter), - new UpdateMapper(converter))); + new UpdateMapper(converter), null)); } @Test // DATAMONGO-1518 @@ -183,6 +191,34 @@ public class DefaultBulkOperationsUnitTests { assertThat(updateModel.getReplacement().getString("lastName")).isEqualTo("Kim"); } + @Test // DATAMONGO-2261 + public void bulkInsertInvokesEntityCallbacks() { + + BeforeConvertPersonCallback beforeConvertCallback = spy(new BeforeConvertPersonCallback()); + BeforeSavePersonCallback beforeSaveCallback = spy(new BeforeSavePersonCallback()); + + ops = new DefaultBulkOperations(template, "collection-1", + new BulkOperationContext(BulkMode.ORDERED, Optional.of(mappingContext.getPersistentEntity(Person.class)), + new QueryMapper(converter), new UpdateMapper(converter), + EntityCallbacks.create(beforeConvertCallback, beforeSaveCallback))); + + Person entity = new Person("init"); + ops.insert(entity); + + ArgumentCaptor personArgumentCaptor = ArgumentCaptor.forClass(Person.class); + verify(beforeConvertCallback).onBeforeConvert(personArgumentCaptor.capture(), eq("collection-1")); + verifyZeroInteractions(beforeSaveCallback); + + ops.execute(); + + verify(beforeSaveCallback).onBeforeSave(personArgumentCaptor.capture(), any(), eq("collection-1")); + assertThat(personArgumentCaptor.getAllValues()).extracting("firstName").containsExactly("init", "before-convert"); + verify(collection).bulkWrite(captor.capture(), any()); + + InsertOneModel updateModel = (InsertOneModel) captor.getValue().get(0); + assertThat(updateModel.getDocument()).containsEntry("firstName", "before-save"); + } + class SomeDomainType { @Id String id; @@ -194,4 +230,22 @@ public class DefaultBulkOperationsUnitTests { enum Gender { M, F } + + static class BeforeConvertPersonCallback implements BeforeConvertCallback { + + @Override + public Person onBeforeConvert(Person entity, String collection) { + return new Person("before-convert"); + } + } + + static class BeforeSavePersonCallback implements BeforeSaveCallback { + + @Override + public Person onBeforeSave(Person entity, Document document, String collection) { + + document.put("firstName", "before-save"); + return new Person("before-save"); + } + } } 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 2884114f8..433e9a1f2 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 @@ -22,6 +22,8 @@ import static org.springframework.data.mongodb.test.util.Assertions.*; import lombok.Data; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -42,9 +44,11 @@ import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -53,6 +57,7 @@ import org.springframework.data.annotation.Version; import org.springframework.data.convert.CustomConversions; import org.springframework.data.domain.Sort; import org.springframework.data.geo.Point; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; @@ -65,7 +70,10 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCre import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; +import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback; +import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; import org.springframework.data.mongodb.core.mapreduce.GroupBy; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.BasicQuery; @@ -74,7 +82,9 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; +import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.CollectionUtils; import com.mongodb.DB; import com.mongodb.MongoClient; @@ -1382,6 +1392,212 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { verify(mapReduceIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build())); } + @Test // DATAMONGO-2261 + public void saveShouldInvokeCallbacks() { + + ValueCapturingBeforeConvertCallback beforeConvertCallback = spy(new ValueCapturingBeforeConvertCallback()); + ValueCapturingBeforeSaveCallback beforeSaveCallback = spy(new ValueCapturingBeforeSaveCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(beforeConvertCallback, beforeSaveCallback)); + + Person entity = new Person(); + entity.id = "init"; + entity.firstname = "luke"; + + template.save(entity); + + verify(beforeConvertCallback).onBeforeConvert(eq(entity), anyString()); + verify(beforeSaveCallback).onBeforeSave(eq(entity), any(), anyString()); + } + + @Test // DATAMONGO-2261 + public void insertShouldInvokeCallbacks() { + + ValueCapturingBeforeConvertCallback beforeConvertCallback = spy(new ValueCapturingBeforeConvertCallback()); + ValueCapturingBeforeSaveCallback beforeSaveCallback = spy(new ValueCapturingBeforeSaveCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(beforeConvertCallback, beforeSaveCallback)); + + Person entity = new Person(); + entity.id = "init"; + entity.firstname = "luke"; + + template.insert(entity); + + verify(beforeConvertCallback).onBeforeConvert(eq(entity), anyString()); + verify(beforeSaveCallback).onBeforeSave(eq(entity), any(), anyString()); + } + + @Test // DATAMONGO-2261 + public void insertAllShouldInvokeCallbacks() { + + ValueCapturingBeforeConvertCallback beforeConvertCallback = spy(new ValueCapturingBeforeConvertCallback()); + ValueCapturingBeforeSaveCallback beforeSaveCallback = spy(new ValueCapturingBeforeSaveCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(beforeConvertCallback, beforeSaveCallback)); + + Person entity1 = new Person(); + entity1.id = "1"; + entity1.firstname = "luke"; + + Person entity2 = new Person(); + entity1.id = "2"; + entity1.firstname = "luke"; + + template.insertAll(Arrays.asList(entity1, entity2)); + + verify(beforeConvertCallback, times(2)).onBeforeConvert(any(), anyString()); + verify(beforeSaveCallback, times(2)).onBeforeSave(any(), any(), anyString()); + } + + @Test // DATAMONGO-2261 + public void findAndReplaceShouldInvokeCallbacks() { + + ValueCapturingBeforeConvertCallback beforeConvertCallback = spy(new ValueCapturingBeforeConvertCallback()); + ValueCapturingBeforeSaveCallback beforeSaveCallback = spy(new ValueCapturingBeforeSaveCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(beforeConvertCallback, beforeSaveCallback)); + + Person entity = new Person(); + entity.id = "init"; + entity.firstname = "luke"; + + template.findAndReplace(new Query(), entity); + + verify(beforeConvertCallback).onBeforeConvert(eq(entity), anyString()); + verify(beforeSaveCallback).onBeforeSave(eq(entity), any(), anyString()); + } + + @Test // DATAMONGO-2261 + public void publishesEventsAndEntityCallbacksInOrder() { + + BeforeConvertCallback beforeConvertCallback = new BeforeConvertCallback() { + + @Override + public Person onBeforeConvert(Person entity, String collection) { + + assertThat(entity.id).isEqualTo("before-convert-event"); + entity.id = "before-convert-callback"; + return entity; + } + }; + + BeforeSaveCallback beforeSaveCallback = new BeforeSaveCallback() { + + @Override + public Person onBeforeSave(Person entity, Document document, String collection) { + + assertThat(entity.id).isEqualTo("before-save-event"); + entity.id = "before-save-callback"; + return entity; + } + }; + + AbstractMongoEventListener eventListener = new AbstractMongoEventListener() { + + @Override + public void onBeforeConvert(BeforeConvertEvent event) { + + assertThat(event.getSource().id).isEqualTo("init"); + event.getSource().id = "before-convert-event"; + } + + @Override + public void onBeforeSave(BeforeSaveEvent event) { + + assertThat(event.getSource().id).isEqualTo("before-convert-callback"); + event.getSource().id = "before-save-event"; + } + }; + + StaticApplicationContext ctx = new StaticApplicationContext(); + ctx.registerBean(ApplicationListener.class, () -> eventListener); + ctx.registerBean(BeforeConvertCallback.class, () -> beforeConvertCallback); + ctx.registerBean(BeforeSaveCallback.class, () -> beforeSaveCallback); + ctx.refresh(); + + template.setApplicationContext(ctx); + + Person entity = new Person(); + entity.id = "init"; + entity.firstname = "luke"; + + Person saved = template.save(entity); + + assertThat(saved.id).isEqualTo("before-save-callback"); + } + + @Test // DATAMONGO-2261 + public void beforeSaveCallbackAllowsTargetDocumentModifications() { + + BeforeSaveCallback beforeSaveCallback = new BeforeSaveCallback() { + + @Override + public Person onBeforeSave(Person entity, Document document, String collection) { + + document.append("added-by", "callback"); + return entity; + } + }; + + StaticApplicationContext ctx = new StaticApplicationContext(); + ctx.registerBean(BeforeSaveCallback.class, () -> beforeSaveCallback); + ctx.refresh(); + + template.setApplicationContext(ctx); + + Person entity = new Person(); + entity.id = "luke-skywalker"; + entity.firstname = "luke"; + + template.save(entity); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Document.class); + + verify(collection).replaceOne(any(), captor.capture(), any(ReplaceOptions.class)); + assertThat(captor.getValue()).containsEntry("added-by", "callback"); + } + + // TODO: additional tests for what is when saved. + + @Test // DATAMONGO-2261 + public void entityCallbacksAreNotSetByDefault() { + Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isNull(); + } + + @Test // DATAMONGO-2261 + public void entityCallbacksShouldBeInitiatedOnSettingApplicationContext() { + + ApplicationContext ctx = new StaticApplicationContext(); + template.setApplicationContext(ctx); + + Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isNotNull(); + } + + @Test // DATAMONGO-2261 + public void setterForEntityCallbackOverridesContextInitializedOnes() { + + ApplicationContext ctx = new StaticApplicationContext(); + template.setApplicationContext(ctx); + + EntityCallbacks callbacks = EntityCallbacks.create(); + template.setEntityCallbacks(callbacks); + + Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isSameAs(callbacks); + } + + @Test // DATAMONGO-2261 + public void setterForApplicationContextShouldNotOverrideAlreadySetEntityCallbacks() { + + EntityCallbacks callbacks = EntityCallbacks.create(); + ApplicationContext ctx = new StaticApplicationContext(); + + template.setEntityCallbacks(callbacks); + template.setApplicationContext(ctx); + + Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isSameAs(callbacks); + } + class AutogenerateableId { @Id BigInteger id; @@ -1500,4 +1716,45 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { protected MongoOperations getOperations() { return this.template; } + + static class ValueCapturingEntityCallback { + + private final List values = new ArrayList<>(1); + + protected void capture(T value) { + values.add(value); + } + + public List getValues() { + return values; + } + + @Nullable + public T getValue() { + return CollectionUtils.lastElement(values); + } + + } + + static class ValueCapturingBeforeConvertCallback extends ValueCapturingEntityCallback + implements BeforeConvertCallback { + + @Override + public Person onBeforeConvert(Person entity, String collection) { + + capture(entity); + return entity; + } + } + + static class ValueCapturingBeforeSaveCallback extends ValueCapturingEntityCallback + implements BeforeSaveCallback { + + @Override + public Person onBeforeSave(Person entity, Document document, String collection) { + + capture(entity); + return entity; + } + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 18f85968d..4e4b5fa12 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -25,6 +25,8 @@ import lombok.Data; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -41,13 +43,18 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.reactivestreams.Publisher; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.StaticApplicationContext; import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.data.mongodb.core.MongoTemplateUnitTests.AutogenerateableId; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback; +import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; @@ -55,7 +62,9 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; +import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.CollectionUtils; import com.mongodb.client.model.CountOptions; import com.mongodb.client.model.CreateCollectionOptions; @@ -65,6 +74,7 @@ import com.mongodb.client.model.FindOneAndReplaceOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.ReplaceOptions; import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.result.UpdateResult; import com.mongodb.reactivestreams.client.AggregatePublisher; import com.mongodb.reactivestreams.client.DistinctPublisher; import com.mongodb.reactivestreams.client.FindPublisher; @@ -72,6 +82,7 @@ import com.mongodb.reactivestreams.client.MapReducePublisher; import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoDatabase; +import com.mongodb.reactivestreams.client.Success; /** * Unit tests for {@link ReactiveMongoTemplate}. @@ -91,8 +102,9 @@ public class ReactiveMongoTemplateUnitTests { @Mock FindPublisher findPublisher; @Mock AggregatePublisher aggregatePublisher; @Mock Publisher runCommandPublisher; - @Mock Publisher updatePublisher; + @Mock Publisher updateResultPublisher; @Mock Publisher findAndUpdatePublisher; + @Mock Publisher successPublisher; @Mock DistinctPublisher distinctPublisher; @Mock Publisher deletePublisher; @Mock MapReducePublisher mapReducePublisher; @@ -115,8 +127,8 @@ public class ReactiveMongoTemplateUnitTests { when(collection.aggregate(anyList())).thenReturn(aggregatePublisher); when(collection.aggregate(anyList(), any(Class.class))).thenReturn(aggregatePublisher); when(collection.count(any(), any(CountOptions.class))).thenReturn(Mono.just(0L)); - when(collection.updateOne(any(), any(), any(UpdateOptions.class))).thenReturn(updatePublisher); - when(collection.updateMany(any(Bson.class), any(), any())).thenReturn(updatePublisher); + when(collection.updateOne(any(), any(), any(UpdateOptions.class))).thenReturn(updateResultPublisher); + when(collection.updateMany(any(Bson.class), any(), any())).thenReturn(updateResultPublisher); when(collection.findOneAndUpdate(any(), any(), any(FindOneAndUpdateOptions.class))) .thenReturn(findAndUpdatePublisher); when(collection.findOneAndReplace(any(Bson.class), any(), any())).thenReturn(findPublisher); @@ -126,6 +138,9 @@ public class ReactiveMongoTemplateUnitTests { when(collection.findOneAndUpdate(any(), any(), any(FindOneAndUpdateOptions.class))) .thenReturn(findAndUpdatePublisher); when(collection.mapReduce(anyString(), anyString(), any())).thenReturn(mapReducePublisher); + when(collection.replaceOne(any(Bson.class), any(), any(ReplaceOptions.class))).thenReturn(updateResultPublisher); + when(collection.insertOne(any(Bson.class))).thenReturn(successPublisher); + when(collection.insertMany(anyList())).thenReturn(successPublisher); when(findPublisher.projection(any())).thenReturn(findPublisher); when(findPublisher.limit(anyInt())).thenReturn(findPublisher); when(findPublisher.collation(any())).thenReturn(findPublisher); @@ -676,6 +691,120 @@ public class ReactiveMongoTemplateUnitTests { is(com.mongodb.client.model.Collation.builder().locale("fr").build())); } + @Test // DATAMONGO-2261 + public void saveShouldInvokeCallbacks() { + + ValueCapturingBeforeConvertCallback beforeConvertCallback = spy(new ValueCapturingBeforeConvertCallback()); + ValueCapturingBeforeSaveCallback beforeSaveCallback = spy(new ValueCapturingBeforeSaveCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(beforeConvertCallback, beforeSaveCallback)); + + Person entity = new Person(); + entity.id = "init"; + entity.firstname = "luke"; + + template.save(entity).subscribe(); + + verify(beforeConvertCallback).onBeforeConvert(eq(entity), anyString()); + verify(beforeSaveCallback).onBeforeSave(eq(entity), any(), anyString()); + } + + @Test // DATAMONGO-2261 + public void insertShouldInvokeCallbacks() { + + ValueCapturingBeforeConvertCallback beforeConvertCallback = spy(new ValueCapturingBeforeConvertCallback()); + ValueCapturingBeforeSaveCallback beforeSaveCallback = spy(new ValueCapturingBeforeSaveCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(beforeConvertCallback, beforeSaveCallback)); + + Person entity = new Person(); + entity.id = "init"; + entity.firstname = "luke"; + + template.insert(entity).subscribe(); + + verify(beforeConvertCallback).onBeforeConvert(eq(entity), anyString()); + verify(beforeSaveCallback).onBeforeSave(eq(entity), any(), anyString()); + } + + @Test // DATAMONGO-2261 + public void insertAllShouldInvokeCallbacks() { + + ValueCapturingBeforeConvertCallback beforeConvertCallback = spy(new ValueCapturingBeforeConvertCallback()); + ValueCapturingBeforeSaveCallback beforeSaveCallback = spy(new ValueCapturingBeforeSaveCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(beforeConvertCallback, beforeSaveCallback)); + + Person entity1 = new Person(); + entity1.id = "1"; + entity1.firstname = "luke"; + + Person entity2 = new Person(); + entity1.id = "2"; + entity1.firstname = "luke"; + + template.insertAll(Arrays.asList(entity1, entity2)).subscribe(); + + verify(beforeConvertCallback, times(2)).onBeforeConvert(any(), anyString()); + verify(beforeSaveCallback, times(2)).onBeforeSave(any(), any(), anyString()); + } + + @Test // DATAMONGO-2261 + public void findAndReplaceShouldInvokeCallbacks() { + + ValueCapturingBeforeConvertCallback beforeConvertCallback = spy(new ValueCapturingBeforeConvertCallback()); + ValueCapturingBeforeSaveCallback beforeSaveCallback = spy(new ValueCapturingBeforeSaveCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(beforeConvertCallback, beforeSaveCallback)); + + Person entity = new Person(); + entity.id = "init"; + entity.firstname = "luke"; + + template.findAndReplace(new Query(), entity).subscribe(); + + verify(beforeConvertCallback).onBeforeConvert(eq(entity), anyString()); + verify(beforeSaveCallback).onBeforeSave(eq(entity), any(), anyString()); + } + + @Test // DATAMONGO-2261 + public void entityCallbacksAreNotSetByDefault() { + Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isNull(); + } + + @Test // DATAMONGO-2261 + public void entityCallbacksShouldBeInitiatedOnSettingApplicationContext() { + + ApplicationContext ctx = new StaticApplicationContext(); + template.setApplicationContext(ctx); + + Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isNotNull(); + } + + @Test // DATAMONGO-2261 + public void setterForEntityCallbackOverridesContextInitializedOnes() { + + ApplicationContext ctx = new StaticApplicationContext(); + template.setApplicationContext(ctx); + + ReactiveEntityCallbacks callbacks = ReactiveEntityCallbacks.create(); + template.setEntityCallbacks(callbacks); + + Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isSameAs(callbacks); + } + + @Test // DATAMONGO-2261 + public void setterForApplicationContextShouldNotOverrideAlreadySetEntityCallbacks() { + + ReactiveEntityCallbacks callbacks = ReactiveEntityCallbacks.create(); + ApplicationContext ctx = new StaticApplicationContext(); + + template.setEntityCallbacks(callbacks); + template.setApplicationContext(ctx); + + Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isSameAs(callbacks); + } + @Data @org.springframework.data.mongodb.core.mapping.Document(collection = "star-wars") static class Person { @@ -714,4 +843,44 @@ public class ReactiveMongoTemplateUnitTests { static class EntityWithListOfSimple { List grades; } + + static class ValueCapturingEntityCallback { + + private final List values = new ArrayList<>(1); + + protected void capture(T value) { + values.add(value); + } + + public List getValues() { + return values; + } + + @Nullable + public T getValue() { + return CollectionUtils.lastElement(values); + } + } + + static class ValueCapturingBeforeConvertCallback extends ValueCapturingEntityCallback + implements ReactiveBeforeConvertCallback { + + @Override + public Mono onBeforeConvert(Person entity, String collection) { + + capture(entity); + return Mono.just(entity); + } + } + + static class ValueCapturingBeforeSaveCallback extends ValueCapturingEntityCallback + implements ReactiveBeforeSaveCallback { + + @Override + public Mono onBeforeSave(Person entity, Document document, String collection) { + + capture(entity); + return Mono.just(entity); + } + } } diff --git a/src/main/asciidoc/reference/mongo-entity-callbacks.adoc b/src/main/asciidoc/reference/mongo-entity-callbacks.adoc new file mode 100644 index 000000000..9d09a5af5 --- /dev/null +++ b/src/main/asciidoc/reference/mongo-entity-callbacks.adoc @@ -0,0 +1,30 @@ += Store specific EntityCallbacks + +Spring Data MongoDB uses the `EntityCallback` API for its auditing support and reacts on the following callbacks. + +.Supported Entity Callbacks +[%header,cols="4"] +|=== +| Callback +| Method +| Description +| Order + +| Reactive/BeforeConvertCallback +| onBeforeConvert(T entity, String collection) +| Invoked before a domain object is converted to `org.bson.Document`. +| `Ordered.LOWEST_PRECEDENCE` + +| Reactive/AuditingEntityCallback +| onBeforeConvert(Object entity, String collection) +| Marks an auditable entity _created_ or _modified_ +| 100 + +| Reactive/BeforeSaveCallback +| onBeforeSave(T entity, org.bson.Document target, String collection) +| Invoked before a domain object is saved. + + Can modify the target, to be persisted, `Document` containing all mapped entity information. +| `Ordered.LOWEST_PRECEDENCE` + +|=== + diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index a48f6408c..56cf44594 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -3159,6 +3159,11 @@ The following callback methods are present in `AbstractMappingEventListener`: NOTE: Lifecycle events are only emitted for root level types. Complex types used as properties within a document root are not subject to event publication unless they are document references annotated with `@DBRef`. +WARNING: Lifecycle events depend on an `ApplicationEventMulticaster`, which in case of the `SimpleApplicationEventMulticaster` can be configured with a `TaskExecutor`, and therefore gives no guarantees when an Event is processed. + +include::../{spring-data-commons-docs}/entity-callbacks.adoc[leveloffset=+1] +include::./mongo-entity-callbacks.adoc[leveloffset=+2] + [[mongo.exception]] == Exception Translation