diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 8a402a2de..5d0e30292 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -34,6 +34,7 @@ import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.relational.core.EntityLifecycleEventDelegate; import org.springframework.data.relational.core.conversion.AggregateChange; import org.springframework.data.relational.core.conversion.BatchingAggregateChange; import org.springframework.data.relational.core.conversion.DeleteAggregateChange; @@ -67,7 +68,7 @@ import org.springframework.util.ClassUtils; */ public class JdbcAggregateTemplate implements JdbcAggregateOperations { - private final ApplicationEventPublisher publisher; + private final EntityLifecycleEventDelegate eventDelegate = new EntityLifecycleEventDelegate(); private final RelationalMappingContext context; private final RelationalEntityDeleteWriter jdbcEntityDeleteWriter; @@ -95,7 +96,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(converter, "RelationalConverter must not be null"); Assert.notNull(dataAccessStrategy, "DataAccessStrategy must not be null"); - this.publisher = publisher; + this.eventDelegate.setPublisher(publisher); this.context = context; this.accessStrategy = dataAccessStrategy; this.converter = converter; @@ -123,7 +124,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(converter, "RelationalConverter must not be null"); Assert.notNull(dataAccessStrategy, "DataAccessStrategy must not be null"); - this.publisher = publisher; + this.eventDelegate.setPublisher(publisher); this.context = context; this.accessStrategy = dataAccessStrategy; this.converter = converter; @@ -145,6 +146,18 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { this.entityCallbacks = entityCallbacks; } + /** + * Configure whether lifecycle events such as {@link AfterSaveEvent}, {@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 3.0 + * @see AbstractRelationalEvent + */ + public void setEntityLifecycleEventsEnabled(boolean enabled) { + this.eventDelegate.setEventsEnabled(enabled); + } + @Override public T save(T instance) { @@ -529,34 +542,32 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { private T triggerAfterConvert(T entity) { - publisher.publishEvent(new AfterConvertEvent<>(entity)); + eventDelegate.publishEvent(() -> new AfterConvertEvent<>(entity)); return entityCallbacks.callback(AfterConvertCallback.class, entity); } private T triggerBeforeConvert(T aggregateRoot) { - publisher.publishEvent(new BeforeConvertEvent<>(aggregateRoot)); - + eventDelegate.publishEvent(() -> new BeforeConvertEvent<>(aggregateRoot)); return entityCallbacks.callback(BeforeConvertCallback.class, aggregateRoot); } private T triggerBeforeSave(T aggregateRoot, AggregateChange change) { - publisher.publishEvent(new BeforeSaveEvent<>(aggregateRoot, change)); + eventDelegate.publishEvent(() -> new BeforeSaveEvent<>(aggregateRoot, change)); return entityCallbacks.callback(BeforeSaveCallback.class, aggregateRoot, change); } private T triggerAfterSave(T aggregateRoot, AggregateChange change) { - publisher.publishEvent(new AfterSaveEvent<>(aggregateRoot, change)); - + eventDelegate.publishEvent(() -> new AfterSaveEvent<>(aggregateRoot, change)); return entityCallbacks.callback(AfterSaveCallback.class, aggregateRoot); } private void triggerAfterDelete(@Nullable T aggregateRoot, Object id, AggregateChange change) { - publisher.publishEvent(new AfterDeleteEvent<>(Identifier.of(id), aggregateRoot, change)); + eventDelegate.publishEvent(() -> new AfterDeleteEvent<>(Identifier.of(id), aggregateRoot, change)); if (aggregateRoot != null) { entityCallbacks.callback(AfterDeleteCallback.class, aggregateRoot); @@ -566,7 +577,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { @Nullable private T triggerBeforeDelete(@Nullable T aggregateRoot, Object id, MutableAggregateChange change) { - publisher.publishEvent(new BeforeDeleteEvent<>(Identifier.of(id), aggregateRoot, change)); + eventDelegate.publishEvent(() -> new BeforeDeleteEvent<>(Identifier.of(id), aggregateRoot, change)); if (aggregateRoot != null) { return entityCallbacks.callback(BeforeDeleteCallback.class, aggregateRoot, change); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateUnitTests.java index 58ae0e696..90da0e68e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateUnitTests.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; @@ -62,7 +63,7 @@ import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback @ExtendWith(MockitoExtension.class) public class JdbcAggregateTemplateUnitTests { - JdbcAggregateOperations template; + JdbcAggregateTemplate template; @Mock DataAccessStrategy dataAccessStrategy; @Mock ApplicationEventPublisher eventPublisher; @@ -97,7 +98,7 @@ public class JdbcAggregateTemplateUnitTests { assertThat(template.findAllById(emptyList(), SampleEntity.class)).isEmpty(); } - @Test // DATAJDBC-393 + @Test // DATAJDBC-393, GH-1291 public void callbackOnSave() { SampleEntity first = new SampleEntity(null, "Alfred"); @@ -112,6 +113,22 @@ public class JdbcAggregateTemplateUnitTests { verify(callbacks).callback(eq(BeforeSaveCallback.class), eq(second), any(MutableAggregateChange.class)); verify(callbacks).callback(AfterSaveCallback.class, third); assertThat(last).isEqualTo(third); + verify(eventPublisher, times(3)).publishEvent(any(Object.class)); + } + + @Test // GH-1291 + public void doesNotEmitEvents() { + + SampleEntity first = new SampleEntity(null, "Alfred"); + SampleEntity second = new SampleEntity(23L, "Alfred E."); + SampleEntity third = new SampleEntity(23L, "Neumann"); + + when(callbacks.callback(any(Class.class), any(), any())).thenReturn(second, third); + + template.setEntityLifecycleEventsEnabled(false); + template.save(first); + + verifyNoInteractions(eventPublisher); } @Test // GH-1137 diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/EntityLifecycleEventDelegate.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/EntityLifecycleEventDelegate.java new file mode 100644 index 000000000..f5151a272 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/EntityLifecycleEventDelegate.java @@ -0,0 +1,63 @@ +/* + * 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.relational.core; + +import java.util.function.Supplier; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.lang.Nullable; + +/** + * Delegate class to encapsulate lifecycle event configuration and publishing. Event creation is deferred within an + * event {@link Supplier} to delay the actual event object creation. + * + * @author Mark Paluch + * @since 3.0 + * @see ApplicationEventPublisher + */ +public 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 eventSupplier the supplier for application events. + */ + public void publishEvent(Supplier eventSupplier) { + + if (canPublishEvent()) { + publisher.publishEvent(eventSupplier.get()); + } + } + + private boolean canPublishEvent() { + return publisher != null && eventsEnabled; + } +} diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index a6432d388..4da445529 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -829,6 +829,10 @@ Note that the type used for prefixing the statement name is the name of the aggr == Lifecycle Events Spring Data JDBC triggers events that get published to any matching `ApplicationListener` beans in the application context. + +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:{javadoc-base}org/springframework/data/jdbc/core/JdbcAggregateTemplate.html#setEntityLifecycleEventsEnabled(boolean)[Template API]. + For example, the following listener gets invoked before an aggregate gets saved: ==== @@ -1100,4 +1104,4 @@ Select * from user u where u.lastname = lastname LOCK IN SHARE MODE ---- ==== -Alternative to `LockMode.PESSIMISTIC_READ` you can use `LockMode.PESSIMISTIC_WRITE`. \ No newline at end of file +Alternative to `LockMode.PESSIMISTIC_READ` you can use `LockMode.PESSIMISTIC_WRITE`.