diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java new file mode 100644 index 000000000..b92b1700b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022 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 org.springframework.context.ApplicationEventPublisher; +import org.springframework.lang.Nullable; + +/** + * Delegate class to encapsulate lifecycle event configuration and publishing. + * + * @author Mark Paluch + * @since 4.0 + * @see ApplicationEventPublisher + */ +class EntityLifecycleEventDelegate { + + private @Nullable ApplicationEventPublisher publisher; + private boolean eventsEnabled = true; + + public void setPublisher(@Nullable ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + public boolean isEventsEnabled() { + return eventsEnabled; + } + + public void setEventsEnabled(boolean eventsEnabled) { + this.eventsEnabled = eventsEnabled; + } + + /** + * Publish an application event if event publishing is enabled. + * + * @param event the application event. + */ + public void publishEvent(Object event) { + + if (canPublishEvent()) { + publisher.publishEvent(event); + } + } + + private boolean canPublishEvent() { + return publisher != null && eventsEnabled; + } +} 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 88269e7ed..debe4b996 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 @@ -23,6 +23,7 @@ import java.math.RoundingMode; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.BiPredicate; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -174,6 +175,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final EntityOperations operations; private final PropertyOperations propertyOperations; private final QueryOperations queryOperations; + private final EntityLifecycleEventDelegate eventDelegate; private @Nullable WriteConcern writeConcern; private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE; @@ -228,6 +230,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext()); this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations, mongoDbFactory); + this.eventDelegate = new EntityLifecycleEventDelegate(); // We always have a mapping context in the converter, whether it's a simple one or not mappingContext = this.mongoConverter.getMappingContext(); @@ -266,6 +269,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.operations = that.operations; this.propertyOperations = that.propertyOperations; this.queryOperations = that.queryOperations; + this.eventDelegate = that.eventDelegate; } /** @@ -308,12 +312,25 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.readPreference = readPreference; } + /** + * Configure whether lifecycle events such as {@link AfterLoadEvent}, {@link BeforeSaveEvent}, etc. should be + * published or whether emission should be suppressed. Enabled by default. + * + * @param enabled {@code true} to enable entity lifecycle events; {@code false} to disable entity lifecycle events. + * @since 4.0 + * @see MongoMappingEvent + */ + public void setEntityLifecycleEventsEnabled(boolean enabled) { + this.eventDelegate.setEventsEnabled(enabled); + } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { prepareIndexCreator(applicationContext); eventPublisher = applicationContext; + eventDelegate.setPublisher(eventPublisher); if (entityCallbacks == null) { setEntityCallbacks(EntityCallbacks.create(applicationContext)); @@ -2168,11 +2185,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } protected , T> E maybeEmitEvent(E event) { - - if (eventPublisher != null) { - eventPublisher.publishEvent(event); - } - + eventDelegate.publishEvent(event); return event; } 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 d5f6ab936..d0102aaee 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 @@ -185,6 +185,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private final EntityOperations operations; private final PropertyOperations propertyOperations; private final QueryOperations queryOperations; + private final EntityLifecycleEventDelegate eventDelegate; private @Nullable WriteConcern writeConcern; private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE; @@ -256,6 +257,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext()); this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations, mongoDatabaseFactory); + this.eventDelegate = new EntityLifecycleEventDelegate(); // We create indexes based on mapping events if (this.mappingContext instanceof MongoMappingContext) { @@ -287,6 +289,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.propertyOperations = that.propertyOperations; this.sessionSynchronization = that.sessionSynchronization; this.queryOperations = that.queryOperations; + this.eventDelegate = that.eventDelegate; } private void onCheckForIndexes(MongoPersistentEntity entity, Consumer subscriptionExceptionHandler) { @@ -339,12 +342,25 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.readPreference = readPreference; } + /** + * Configure whether lifecycle events such as {@link AfterLoadEvent}, {@link BeforeSaveEvent}, etc. should be + * published or whether emission should be suppressed. Enabled by default. + * + * @param enabled {@code true} to enable entity lifecycle events; {@code false} to disable entity lifecycle events. + * @since 4.0 + * @see MongoMappingEvent + */ + public void setEntityLifecycleEventsEnabled(boolean enabled) { + this.eventDelegate.setEventsEnabled(enabled); + } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { prepareIndexCreator(applicationContext); eventPublisher = applicationContext; + eventDelegate.setPublisher(eventPublisher); if (entityCallbacks == null) { setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext)); @@ -2324,11 +2340,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } protected , T> E maybeEmitEvent(E event) { - - if (eventPublisher != null) { - eventPublisher.publishEvent(event); - } - + eventDelegate.publishEvent(event); return event; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java index 6e2ddb15d..e532042f1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java @@ -1616,6 +1616,21 @@ public class ReactiveMongoTemplateTests { assertThat(saved.get().id).isNotNull(); } + @Test // GH-4107 + void afterSaveEventCanBeDisabled() { + + AtomicReference saved = createAfterSaveReference(); + ImmutableVersioned source = new ImmutableVersioned(); + + template.setEntityLifecycleEventsEnabled(false); + template.insertAll(Collections.singleton(new ImmutableVersioned())) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + assertThat(saved).hasValue(null); + } + @Test // DATAMONGO-2012 @EnableIfMongoServerVersion(isGreaterThanEqual = "4.0") @EnableIfReplicaSetAvailable diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java index 761f8f174..3c6569c72 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java @@ -47,6 +47,7 @@ import org.springframework.data.mongodb.repository.support.MongoRepositoryFactor import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor; import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.test.annotation.DirtiesContext; import com.mongodb.WriteConcern; import com.mongodb.client.MongoClient; @@ -161,6 +162,22 @@ public class ApplicationContextEventTests { assertThat(listener.onAfterConvertEvents.get(0).getCollectionName()).isEqualTo(COLLECTION_NAME); } + @Test // GH-4107 + @DirtiesContext + public void configurationShouldDisableLifecycleEvents() { + + template.setEntityLifecycleEventsEnabled(false); + + PersonPojoStringId entity = new PersonPojoStringId("1", "Text"); + template.insert(entity); + + template.findOne(query(where("id").is(entity.getId())), PersonPojoStringId.class); + + assertThat(listener.onAfterLoadEvents).isEmpty(); + assertThat(listener.onBeforeConvertEvents).isEmpty(); + assertThat(listener.onAfterConvertEvents).isEmpty(); + } + @Test // DATAMONGO-1256 public void loadEventsOnAggregation() { diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index a8caa7b0e..ea545122a 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -2537,9 +2537,15 @@ You can get at the MongoDB driver's `MongoDatabase.runCommand( )` method by usin [[mongodb.mapping-usage.events]] == Lifecycle Events -The MongoDB mapping framework includes several `org.springframework.context.ApplicationEvent` events that your application can respond to by registering special beans in the `ApplicationContext`. Being based on Spring's `ApplicationContext` event infrastructure enables other products, such as Spring Integration, to easily receive these events, as they are a well known eventing mechanism in Spring-based applications. +The MongoDB mapping framework includes several `org.springframework.context.ApplicationEvent` events that your application can respond to by registering special beans in the `ApplicationContext`. +Being based on Spring's `ApplicationContext` event infrastructure enables other products, such as Spring Integration, to easily receive these events, as they are a well known eventing mechanism in Spring-based applications. -To intercept an object before it goes through the conversion process (which turns your domain object into a `org.bson.Document`), you can register a subclass of `AbstractMongoEventListener` that overrides the `onBeforeConvert` method. When the event is dispatched, your listener is called and passed the domain object before it goes into the converter. The following example shows how to do so: +Entity lifecycle events can be costly and you may notice a change in the performance profile when loading large result sets. +You can disable lifecycle events on the link:https://docs.spring.io/spring-data/mongodb/docs/{version}/api/org/springframework/data/mongodb/core/MongoTemplate.html#setEntityLifecycleEventsEnabled(boolean)[Template API]. + +To intercept an object before it goes through the conversion process (which turns your domain object into a `org.bson.Document`), you can register a subclass of `AbstractMongoEventListener` that overrides the `onBeforeConvert` method. +When the event is dispatched, your listener is called and passed the domain object before it goes into the converter. +The following example shows how to do so: ==== [source,java]