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 e409c06b..5246a4cb 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 @@ -16,10 +16,15 @@ package org.springframework.data.jdbc.core; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.mapping.IdentifierAccessor; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.conversion.AggregateChange; import org.springframework.data.relational.core.conversion.AggregateChange.Kind; import org.springframework.data.relational.core.conversion.Interpreter; @@ -30,12 +35,7 @@ import org.springframework.data.relational.core.conversion.RelationalEntityUpdat import org.springframework.data.relational.core.conversion.RelationalEntityWriter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.relational.core.mapping.event.AfterDeleteEvent; -import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; -import org.springframework.data.relational.core.mapping.event.AfterSaveEvent; -import org.springframework.data.relational.core.mapping.event.BeforeDeleteEvent; -import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.relational.core.mapping.event.Identifier; +import org.springframework.data.relational.core.mapping.event.*; import org.springframework.data.relational.core.mapping.event.Identifier.Specified; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -62,6 +62,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { private final DataAccessStrategy accessStrategy; + private EntityCallbacks entityCallbacks = NoopEntityCallback.INSTANCE; + /** * Creates a new {@link JdbcAggregateTemplate} given {@link ApplicationEventPublisher}, * {@link RelationalMappingContext} and {@link DataAccessStrategy}. @@ -90,6 +92,13 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy); } + public void setEntityCallbacks(EntityCallbacks entityCallbacks) { + + Assert.notNull(entityCallbacks, "Callbacks must not be null."); + + this.entityCallbacks = entityCallbacks; + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#save(java.lang.Object) @@ -100,11 +109,11 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(instance, "Aggregate instance must not be null!"); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); - IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(instance); - AggregateChange change = createChange(instance); + Function> changeCreator = persistentEntity.isNew(instance) ? this::createInsertChange + : this::createUpdateChange; - return store(instance, identifierAccessor, change, persistentEntity); + return store(instance, changeCreator, persistentEntity); } /** @@ -120,11 +129,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(instance, "Aggregate instance must not be null!"); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); - IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(instance); - AggregateChange change = createInsertChange(instance); - - return store(instance, identifierAccessor, change, persistentEntity); + return store(instance, this::createInsertChange, persistentEntity); } /** @@ -140,11 +146,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(instance, "Aggregate instance must not be null!"); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); - IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(instance); - AggregateChange change = createUpdateChange(instance); - - return store(instance, identifierAccessor, change, persistentEntity); + return store(instance, this::createUpdateChange, persistentEntity); } /* @@ -171,7 +174,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { T entity = accessStrategy.findById(id, domainType); if (entity != null) { - publishAfterLoad(id, entity); + return triggerAfterLoad(id, entity); } return entity; } @@ -199,8 +202,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(domainType, "Domain type must not be null!"); Iterable all = accessStrategy.findAll(domainType); - publishAfterLoad(all); - return all; + return triggerAfterLoad(all); } /* @@ -214,8 +216,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(domainType, "Domain type must not be null!"); Iterable allById = accessStrategy.findAllById(ids, domainType); - publishAfterLoad(allById); - return allById; + return triggerAfterLoad(allById); } /* @@ -260,16 +261,20 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { change.executeWith(interpreter, context, converter); } - private T store(T instance, IdentifierAccessor identifierAccessor, AggregateChange change, + private T store(T aggregateRoot, Function> changeCreator, RelationalPersistentEntity persistentEntity) { - Assert.notNull(instance, "Aggregate instance must not be null!"); + Assert.notNull(aggregateRoot, "Aggregate instance must not be null!"); - publisher.publishEvent(new BeforeSaveEvent( // - Identifier.ofNullable(identifierAccessor.getIdentifier()), // - instance, // - change // - )); + aggregateRoot = triggerBeforeConvert(aggregateRoot, + persistentEntity.getIdentifierAccessor(aggregateRoot).getIdentifier()); + + AggregateChange change = changeCreator.apply(aggregateRoot); + + aggregateRoot = triggerBeforeSave(aggregateRoot, + persistentEntity.getIdentifierAccessor(aggregateRoot).getIdentifier(), change); + + change.setEntity(aggregateRoot); change.executeWith(interpreter, context, converter); @@ -277,31 +282,26 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(identifier, "After saving the identifier must not be null!"); - publisher.publishEvent(new AfterSaveEvent( // - Identifier.of(identifier), // - change.getEntity(), // - change // - )); - - return (T) change.getEntity(); + return triggerAfterSave(change.getEntity(), identifier, change); } - private void deleteTree(Object id, @Nullable Object entity, Class domainType) { + private void deleteTree(Object id, @Nullable T entity, Class domainType) { - AggregateChange change = createDeletingChange(id, entity, domainType); + AggregateChange change = createDeletingChange(id, entity, domainType); - Specified specifiedId = Identifier.of(id); - Optional optionalEntity = Optional.ofNullable(entity); - publisher.publishEvent(new BeforeDeleteEvent(specifiedId, optionalEntity, change)); + entity = triggerBeforeDelete(entity, id, change); + change.setEntity(entity); change.executeWith(interpreter, context, converter); - publisher.publishEvent(new AfterDeleteEvent(specifiedId, optionalEntity, change)); + triggerAfterDelete(entity, id, change); } @SuppressWarnings({ "unchecked", "rawtypes" }) private AggregateChange createChange(T instance) { + // context.getRequiredPersistentEntity(o.getClass()).isNew(o) + AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, instance.getClass(), instance); jdbcEntityWriter.write(instance, aggregateChange); return aggregateChange; @@ -324,9 +324,9 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { } @SuppressWarnings({ "unchecked", "rawtypes" }) - private AggregateChange createDeletingChange(Object id, @Nullable Object entity, Class domainType) { + private AggregateChange createDeletingChange(Object id, @Nullable T entity, Class domainType) { - AggregateChange aggregateChange = new AggregateChange(Kind.DELETE, domainType, entity); + AggregateChange aggregateChange = new AggregateChange(Kind.DELETE, domainType, entity); jdbcEntityDeleteWriter.write(id, aggregateChange); return aggregateChange; } @@ -338,18 +338,96 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { return aggregateChange; } - private void publishAfterLoad(Iterable all) { + private Iterable triggerAfterLoad(Iterable all) { - for (T e : all) { + return StreamSupport.stream(all.spliterator(), false).map(e -> { RelationalPersistentEntity entity = context.getRequiredPersistentEntity(e.getClass()); IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(e); - publishAfterLoad(identifierAccessor.getRequiredIdentifier(), e); + return triggerAfterLoad(identifierAccessor.getRequiredIdentifier(), e); + }).collect(Collectors.toList()); + } + + private T triggerAfterLoad(Object id, T entity) { + + publisher.publishEvent(new AfterLoadEvent(Identifier.of(id), entity)); + + return entityCallbacks.callback(AfterLoadCallback.class, entity, Identifier.of(id)); + } + + private T triggerBeforeConvert(T aggregateRoot, @Nullable Object id) { + + Identifier identifier = Identifier.ofNullable(id); + + return entityCallbacks.callback(BeforeConvertCallback.class, aggregateRoot, identifier); + } + + private T triggerBeforeSave(T aggregateRoot, @Nullable Object id, AggregateChange change) { + + Identifier identifier = Identifier.ofNullable(id); + + publisher.publishEvent(new BeforeSaveEvent( // + identifier, // + aggregateRoot, // + change // + )); + + return entityCallbacks.callback(BeforeSaveCallback.class, aggregateRoot, identifier); + } + + private T triggerAfterSave(T aggregateRoot, Object id, AggregateChange change) { + + Specified identifier = Identifier.of(id); + + publisher.publishEvent(new AfterSaveEvent( // + identifier, // + aggregateRoot, // + change // + )); + + return entityCallbacks.callback(AfterSaveCallback.class, aggregateRoot, identifier); + } + + @Nullable + private void triggerAfterDelete(@Nullable T aggregateRoot, Object id, AggregateChange change) { + + Specified identifier = Identifier.of(id); + + publisher.publishEvent(new AfterDeleteEvent(identifier, Optional.ofNullable(aggregateRoot), change)); + + if (aggregateRoot != null) { + entityCallbacks.callback(AfterDeleteCallback.class, aggregateRoot, identifier); } } - private void publishAfterLoad(Object id, T entity) { - publisher.publishEvent(new AfterLoadEvent(Identifier.of(id), entity)); + @Nullable + private T triggerBeforeDelete(@Nullable T aggregateRoot, Object id, AggregateChange change) { + + Specified identifier = Identifier.of(id); + + publisher.publishEvent(new BeforeDeleteEvent(identifier, Optional.ofNullable(aggregateRoot), change)); + + if (aggregateRoot != null) { + return entityCallbacks.callback(BeforeDeleteCallback.class, aggregateRoot, identifier); + } + return aggregateRoot; + } + + /** + * An {@link EntityCallbacks} implementation doing nothing. + */ + private enum NoopEntityCallback implements EntityCallbacks { + + INSTANCE { + + @Override + public void addEntityCallback(EntityCallback callback) {} + + @Override + public T callback(Class callbackType, T entity, Object... args) { + return entity; + } + } } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java index 4bdd8eb4..643fbdbb 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java @@ -24,6 +24,7 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.data.auditing.IsNewAwareAuditingHandler; import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; import org.springframework.data.auditing.config.AuditingConfiguration; +import org.springframework.data.relational.domain.support.RelationalAuditingCallback; import org.springframework.data.relational.domain.support.RelationalAuditingEventListener; import org.springframework.util.Assert; @@ -86,7 +87,7 @@ class JdbcAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition, BeanDefinitionRegistry registry) { - Class listenerClass = RelationalAuditingEventListener.class; + Class listenerClass = RelationalAuditingCallback.class; BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(listenerClass) // .addConstructorArgReference(AUDITING_HANDLER_BEAN_NAME); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index 1536a166..c2a5e852 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -23,6 +23,7 @@ import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.jdbc.repository.RowMapperMap; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.repository.core.EntityInformation; @@ -53,6 +54,7 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { private final NamedParameterJdbcOperations operations; private QueryMappingConfiguration queryMappingConfiguration = QueryMappingConfiguration.EMPTY; + private EntityCallbacks entityCallbacks; /** * Creates a new {@link JdbcRepositoryFactory} for the given {@link DataAccessStrategy}, @@ -117,7 +119,13 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { JdbcAggregateTemplate template = new JdbcAggregateTemplate(publisher, context, converter, accessStrategy); - return new SimpleJdbcRepository<>(template, context.getPersistentEntity(repositoryInformation.getDomainType())); + SimpleJdbcRepository repository = new SimpleJdbcRepository<>(template, context.getPersistentEntity(repositoryInformation.getDomainType())); + + if (entityCallbacks != null) { + template.setEntityCallbacks(entityCallbacks); + } + + return repository; } /* @@ -147,4 +155,9 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); } + + public void setEntityCallbacks(EntityCallbacks entityCallbacks) { + + this.entityCallbacks = entityCallbacks; + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index 2948d680..82493ce8 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -27,6 +27,7 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.jdbc.repository.RowMapperMap; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -54,6 +55,7 @@ public class JdbcRepositoryFactoryBean, S, ID extend private DataAccessStrategy dataAccessStrategy; private QueryMappingConfiguration queryMappingConfiguration = QueryMappingConfiguration.EMPTY; private NamedParameterJdbcOperations operations; + private EntityCallbacks entityCallbacks; /** * Creates a new {@link JdbcRepositoryFactoryBean} for the given repository interface. @@ -85,6 +87,7 @@ public class JdbcRepositoryFactoryBean, S, ID extend JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcRepositoryFactory(dataAccessStrategy, mappingContext, converter, publisher, operations); jdbcRepositoryFactory.setQueryMappingConfiguration(queryMappingConfiguration); + jdbcRepositoryFactory.setEntityCallbacks(entityCallbacks); return jdbcRepositoryFactory; } @@ -151,10 +154,16 @@ public class JdbcRepositoryFactoryBean, S, ID extend Assert.state(this.converter != null, "RelationalConverter is required and must not be null!"); if (this.operations == null) { + + Assert.state(beanFactory != null, "If no JdbcOperations are set a BeanFactory must be available."); + this.operations = beanFactory.getBean(NamedParameterJdbcOperations.class); } if (this.dataAccessStrategy == null) { + + Assert.state(beanFactory != null, "If no DataAccessStrategy is set a BeanFactory must be available."); + this.dataAccessStrategy = this.beanFactory.getBeanProvider(DataAccessStrategy.class) // .getIfAvailable(() -> { @@ -168,6 +177,10 @@ public class JdbcRepositoryFactoryBean, S, ID extend this.queryMappingConfiguration = QueryMappingConfiguration.EMPTY; } + if (beanFactory != null) { + entityCallbacks = EntityCallbacks.create(beanFactory); + } + super.afterPropertiesSet(); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java index 15093f3f..3786469a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java @@ -44,6 +44,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.testing.DatabaseProfileValueSource; import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -71,6 +72,7 @@ public class JdbcAggregateTemplateIntegrationTests { @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + @Autowired JdbcAggregateOperations template; @Autowired NamedParameterJdbcOperations jdbcTemplate; LegoSet legoSet = createLegoSet(); @@ -728,22 +730,6 @@ public class JdbcAggregateTemplateIntegrationTests { private String content; } - @Configuration - @Import(TestConfiguration.class) - static class Config { - - @Bean - Class testClass() { - return JdbcAggregateTemplateIntegrationTests.class; - } - - @Bean - JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context, - DataAccessStrategy dataAccessStrategy, RelationalConverter converter) { - return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); - } - } - /** * One may think of ChainN as a chain with N further elements */ @@ -869,4 +855,21 @@ public class JdbcAggregateTemplateIntegrationTests { String fourValue; Map chain3 = new HashMap<>(); } + + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Bean + Class testClass() { + return JdbcAggregateTemplateIntegrationTests.class; + } + + @Bean + JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context, + DataAccessStrategy dataAccessStrategy, RelationalConverter converter) { + return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); + } + } } 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 cdc74b87..c50fd0e5 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 @@ -15,13 +15,14 @@ */ package org.springframework.data.jdbc.core; +import static java.util.Arrays.*; import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import lombok.AllArgsConstructor; import lombok.Data; -import javax.sql.DataSource; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,15 +32,19 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.RelationResolver; -import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.data.relational.core.mapping.event.AfterDeleteCallback; +import org.springframework.data.relational.core.mapping.event.AfterLoadCallback; +import org.springframework.data.relational.core.mapping.event.AfterSaveCallback; +import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.relational.core.mapping.event.BeforeDeleteCallback; +import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback; +import org.springframework.data.relational.core.mapping.event.Identifier; /** * @author Christoph Strobl @@ -49,19 +54,19 @@ public class JdbcAggregateTemplateUnitTests { JdbcAggregateOperations template; + @Mock DataAccessStrategy dataAccessStrategy; @Mock ApplicationEventPublisher eventPublisher; @Mock RelationResolver relationResolver; - @Mock DataSource dataSource; + @Mock EntityCallbacks callbacks; @Before public void setUp() { RelationalMappingContext mappingContext = new RelationalMappingContext(NamingStrategy.INSTANCE); JdbcConverter converter = new BasicJdbcConverter(mappingContext, relationResolver); - NamedParameterJdbcOperations namedParameterJdbcOperations = new NamedParameterJdbcTemplate(dataSource); - DataAccessStrategy dataAccessStrategy = new DefaultDataAccessStrategy(new SqlGeneratorSource(mappingContext), - mappingContext, converter, namedParameterJdbcOperations); + template = new JdbcAggregateTemplate(eventPublisher, mappingContext, converter, dataAccessStrategy); + ((JdbcAggregateTemplate) template).setEntityCallbacks(callbacks); } @Test // DATAJDBC-378 @@ -81,7 +86,61 @@ public class JdbcAggregateTemplateUnitTests { assertThat(template.findAllById(emptyList(), SampleEntity.class)).isEmpty(); } + @Test // DATAJDBC-393 + public void callbackOnSave() { + + 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); + + SampleEntity last = template.save(first); + + verify(callbacks).callback(BeforeConvertCallback.class, first, Identifier.ofNullable(null)); + verify(callbacks).callback(BeforeSaveCallback.class, second, Identifier.ofNullable(23L)); + verify(callbacks).callback(AfterSaveCallback.class, third, Identifier.of(23L)); + assertThat(last).isEqualTo(third); + } + + @Test // DATAJDBC-393 + public void callbackOnDelete() { + + SampleEntity first = new SampleEntity(23L, "Alfred"); + SampleEntity second = new SampleEntity(23L, "Alfred E."); + + when(callbacks.callback(any(Class.class), any(), any())).thenReturn(second); + + template.delete(first, SampleEntity.class); + + verify(callbacks).callback(BeforeDeleteCallback.class, first, Identifier.of(23L)); + verify(callbacks).callback(AfterDeleteCallback.class, second, Identifier.of(23L)); + } + + @Test // DATAJDBC-393 + public void callbackOnLoad() { + + SampleEntity alfred1 = new SampleEntity(23L, "Alfred"); + SampleEntity alfred2 = new SampleEntity(23L, "Alfred E."); + + SampleEntity neumann1 = new SampleEntity(42L, "Neumann"); + SampleEntity neumann2 = new SampleEntity(42L, "Alfred E. Neumann"); + + when(dataAccessStrategy.findAll(SampleEntity.class)).thenReturn(asList(alfred1, neumann1)); + + when(callbacks.callback(any(Class.class), eq(alfred1), any())).thenReturn(alfred2); + when(callbacks.callback(any(Class.class), eq(neumann1), any())).thenReturn(neumann2); + + Iterable all = template.findAll(SampleEntity.class); + + verify(callbacks).callback(AfterLoadCallback.class, alfred1, Identifier.of(23L)); + verify(callbacks).callback(AfterLoadCallback.class, neumann1, Identifier.of(42L)); + + assertThat(all).containsExactly(alfred2, neumann2); + } + @Data + @AllArgsConstructor private static class SampleEntity { @Column("id1") @Id private Long id; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java index 962f4b54..ef66345a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java @@ -20,6 +20,9 @@ import static org.assertj.core.api.Assertions.*; import lombok.Data; import lombok.Value; import lombok.experimental.FieldDefaults; +import lombok.experimental.Wither; + +import java.util.concurrent.atomic.AtomicLong; import org.junit.ClassRule; import org.junit.Rule; @@ -34,6 +37,7 @@ import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; import org.springframework.data.relational.core.mapping.NamingStrategy; +import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback; import org.springframework.data.repository.CrudRepository; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.test.context.ContextConfiguration; @@ -67,6 +71,7 @@ public class JdbcRepositoryIdGenerationIntegrationTests { @Autowired NamedParameterJdbcTemplate template; @Autowired ReadOnlyIdEntityRepository readOnlyIdrepository; @Autowired PrimitiveIdEntityRepository primitiveIdRepository; + @Autowired ImmutableWithManualIdEntityRepository immutableWithManualIdEntityRepository; @Test // DATAJDBC-98 public void idWithoutSetterGetsSet() { @@ -99,10 +104,23 @@ public class JdbcRepositoryIdGenerationIntegrationTests { }); } + @Test // DATAJDBC-393 + public void manuallyGeneratedId() { + + ImmutableWithManualIdEntity entity = new ImmutableWithManualIdEntity(null, "immutable"); + ImmutableWithManualIdEntity saved = immutableWithManualIdEntityRepository.save(entity); + + assertThat(saved.getId()).isNotNull(); + + assertThat(immutableWithManualIdEntityRepository.findAll()).hasSize(1); + } + private interface PrimitiveIdEntityRepository extends CrudRepository {} public interface ReadOnlyIdEntityRepository extends CrudRepository {} + private interface ImmutableWithManualIdEntityRepository extends CrudRepository {} + @Value @FieldDefaults(makeFinal = false) static class ReadOnlyIdEntity { @@ -118,11 +136,20 @@ public class JdbcRepositoryIdGenerationIntegrationTests { String name; } + @Value + @Wither + static class ImmutableWithManualIdEntity { + @Id Long id; + String name; + } + @Configuration @ComponentScan("org.springframework.data.jdbc.testing") @EnableJdbcRepositories(considerNestedRepositories = true) static class TestConfiguration { + AtomicLong lastId = new AtomicLong(0); + @Bean Class testClass() { return JdbcRepositoryIdGenerationIntegrationTests.class; @@ -134,12 +161,19 @@ public class JdbcRepositoryIdGenerationIntegrationTests { */ @Bean NamingStrategy namingStrategy() { + return new NamingStrategy() { + @Override public String getTableName(Class type) { return type.getSimpleName().toUpperCase(); } }; } + + @Bean + BeforeConvertCallback idGenerator() { + return (e, __) -> e.withId(lastId.incrementAndGet()); + } } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java index 3f8b2c0d..be6e3f8a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java @@ -42,7 +42,9 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.domain.AuditorAware; import org.springframework.data.relational.core.mapping.NamingStrategy; +import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback; import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent; +import org.springframework.data.relational.core.mapping.event.Identifier; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.springframework.test.context.ActiveProfiles; @@ -186,7 +188,9 @@ public class EnableJdbcAuditingHsqlIntegrationTests { AuditingAnnotatedDummyEntityRepository.class, // TestConfiguration.class, // AuditingConfiguration.class, // - OrderAssertingEventListener.class) // + OrderAssertingEventListener.class, // + OrderAssertingCallback.class // + ) // .accept(repository -> { AuditingAnnotatedDummyEntity entity = repository.save(new AuditingAnnotatedDummyEntity()); @@ -331,4 +335,21 @@ public class EnableJdbcAuditingHsqlIntegrationTests { assertThat(((AuditingAnnotatedDummyEntity) entity).createdDate).isNotNull(); } } + + /** + * An event listener asserting that it is running after {@link AuditingConfiguration#auditorAware()} was invoked and + * set the auditing data. + */ + @Component + static class OrderAssertingCallback implements BeforeConvertCallback { + + @Override + public Object onBeforeConvert(Object entity, Identifier id) { + + assertThat(entity).isInstanceOf(AuditingAnnotatedDummyEntity.class); + assertThat(((AuditingAnnotatedDummyEntity) entity).createdDate).isNotNull(); + + return entity; + } + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java index 3a00809a..15a0b266 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java @@ -23,10 +23,10 @@ import java.util.function.Supplier; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; - import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ObjectProvider; @@ -35,7 +35,6 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.JdbcTypeFactory; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -60,7 +59,7 @@ public class JdbcRepositoryFactoryBeanUnitTests { @Mock DataAccessStrategy dataAccessStrategy; @Mock ApplicationEventPublisher publisher; - @Mock ListableBeanFactory beanFactory; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) ListableBeanFactory beanFactory; RelationalMappingContext mappingContext; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 5121d07e..623ddb9b 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -20,6 +20,8 @@ import java.util.Optional; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationEventPublisher; @@ -38,6 +40,7 @@ import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; diff --git a/spring-data-jdbc/src/test/resources/logback.xml b/spring-data-jdbc/src/test/resources/logback.xml index f1bfdbaf..7a20d1ac 100644 --- a/spring-data-jdbc/src/test/resources/logback.xml +++ b/spring-data-jdbc/src/test/resources/logback.xml @@ -8,7 +8,7 @@ - + diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-hsql.sql index 1618e256..225c54e8 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-hsql.sql @@ -1,4 +1,5 @@ -- noinspection SqlNoDataSourceInspectionForFile -CREATE TABLE ReadOnlyIdEntity (ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(100)) -CREATE TABLE PrimitiveIdEntity (ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(100)) +CREATE TABLE ReadOnlyIdEntity (ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE PrimitiveIdEntity (ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE ImmutableWithManualIdentity (ID BIGINT PRIMARY KEY, NAME VARCHAR(100)); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java index 6150a2c8..9cdbdbd3 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java @@ -99,6 +99,11 @@ public class AggregateChange { } } + + public void setEntity(@Nullable T aggregateRoot) { + entity = aggregateRoot; + } + @SuppressWarnings("unchecked") private static void setIdInElementOfSet(RelationalConverter converter, DbAction.WithDependingOn action, Object generatedId, Set set) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterDeleteCallback.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterDeleteCallback.java new file mode 100644 index 00000000..6ac2b17b --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterDeleteCallback.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 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.mapping.event; + +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.lang.Nullable; + +/** + * An {@link EntityCallback} that gets called after an aggregate got deleted. This callback gets only invoked if + * the method deleting the aggregate received an instance of that aggregate as an argument. Methods deleting entities by id or + * without any parameter don't invoke this callback. + * + * @since 1.1 + * @author Jens Schauder + */ +@FunctionalInterface +public interface AfterDeleteCallback extends EntityCallback { + + T onAfterDelete(@Nullable T aggregate, Identifier id); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterDeleteEvent.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterDeleteEvent.java index 16477ecf..9b9bcfd5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterDeleteEvent.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterDeleteEvent.java @@ -36,7 +36,7 @@ public class AfterDeleteEvent extends RelationalEventWithId { * @param change the {@link AggregateChange} encoding the actions that were performed on the database as part of the * delete operation. */ - public AfterDeleteEvent(Specified id, Optional instance, AggregateChange change) { + public AfterDeleteEvent(Specified id, Optional instance, AggregateChange change) { super(id, instance, change); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterLoadCallback.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterLoadCallback.java new file mode 100644 index 00000000..2bad180e --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterLoadCallback.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.mapping.event; + +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * An {@link EntityCallback} that gets invoked after an aggregate gets loaded from the database. + * + * @since 1.1 + * @author Jens Schauder + */ +@FunctionalInterface +public interface AfterLoadCallback extends EntityCallback { + T onAfterLoad(T aggregate, Identifier.Specified id); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterSaveCallback.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterSaveCallback.java new file mode 100644 index 00000000..e5f65756 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/AfterSaveCallback.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.mapping.event; + +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * An {@link EntityCallback} that gets invoked after an aggregate was saved. + * + * @since 1.1 + * @author Jens Schauder + */ +@FunctionalInterface +public interface AfterSaveCallback extends EntityCallback { + T onAfterSave(T aggregate, Identifier.Specified id); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeConvertCallback.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeConvertCallback.java new file mode 100644 index 00000000..82ea8d45 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeConvertCallback.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 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.mapping.event; + +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * An {@link EntityCallback} that gets invoked before the aggregate gets converted into a database change. The decision + * if the change will be an insert or update will be made before this callback gets called. + * + * @since 1.1 + * @author Jens Schauder + */ +@FunctionalInterface +public interface BeforeConvertCallback extends EntityCallback { + T onBeforeConvert(T aggregate, Identifier id); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeConvertEvent.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeConvertEvent.java new file mode 100644 index 00000000..c9b50ef7 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeConvertEvent.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2019 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.mapping.event; + +import org.springframework.data.relational.core.conversion.AggregateChange; +import org.springframework.data.relational.core.mapping.event.Identifier.Specified; + +/** + * Gets published before an aggregate gets converted into a database change. + * + * @since 1.1 + * @author Jens Schauder + */ +public class BeforeConvertEvent extends RelationalEventWithIdAndEntity { + + private static final long serialVersionUID = 3980149746683849019L; + + /** + * @param id identifier of the saved entity. + * @param instance the saved entity. + * @param change the {@link AggregateChange} encoding the actions performed on the database as part of the delete. + */ + public BeforeConvertEvent(Specified id, Object instance, AggregateChange change) { + super(id, instance, change); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeDeleteCallback.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeDeleteCallback.java new file mode 100644 index 00000000..40d9cd52 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeDeleteCallback.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 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.mapping.event; + +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * An {@link EntityCallback} that gets invoked before an entity gets deleted.This callback gets only invoked if * the + * method deleting the aggregate received an instance of that aggregate as an argument. Methods deleting entities by id + * or * without any parameter don't invoke this callback. + * + * @since 1.1 + * @author Jens Schauder + */ +@FunctionalInterface +public interface BeforeDeleteCallback extends EntityCallback { + + T onBeforeDelete(T aggregate, Identifier id); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeDeleteEvent.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeDeleteEvent.java index aae5ec56..6436da8d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeDeleteEvent.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeDeleteEvent.java @@ -35,7 +35,7 @@ public class BeforeDeleteEvent extends RelationalEventWithId { * @param entity the entity about to get deleted. Might be empty. * @param change the {@link AggregateChange} encoding the planned actions to be performed on the database. */ - public BeforeDeleteEvent(Specified id, Optional entity, AggregateChange change) { + public BeforeDeleteEvent(Specified id, Optional entity, AggregateChange change) { super(id, entity, change); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeSaveCallback.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeSaveCallback.java new file mode 100644 index 00000000..479d3f12 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeSaveCallback.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 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.mapping.event; + +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * An {@link EntityCallback} that gets invoked before changes get applied to the database but after the aggregate got actually converted to a database change. + * + * @since 1.1 + * @author Jens Schauder + */ +@FunctionalInterface +public interface BeforeSaveCallback extends EntityCallback { + + T onBeforeSave(T aggregate, Identifier id); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/RelationalEventWithId.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/RelationalEventWithId.java index 6570b0b9..2d890453 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/RelationalEventWithId.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/RelationalEventWithId.java @@ -32,7 +32,7 @@ public class RelationalEventWithId extends SimpleRelationalEvent implements With private final Specified id; - public RelationalEventWithId(Specified id, Optional entity, @Nullable AggregateChange change) { + public RelationalEventWithId(Specified id, Optional entity, @Nullable AggregateChange change) { super(id, entity, change); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/SimpleRelationalEvent.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/SimpleRelationalEvent.java index efdcb50a..bcd49297 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/SimpleRelationalEvent.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/SimpleRelationalEvent.java @@ -35,7 +35,7 @@ class SimpleRelationalEvent extends ApplicationEvent implements RelationalEvent private final Object entity; private final AggregateChange change; - SimpleRelationalEvent(Identifier id, Optional entity, @Nullable AggregateChange change) { + SimpleRelationalEvent(Identifier id, Optional entity, @Nullable AggregateChange change) { super(id); @@ -61,6 +61,15 @@ class SimpleRelationalEvent extends ApplicationEvent implements RelationalEvent return Optional.ofNullable(entity); } + /** + * Returns the an {@link AggregateChange} instance representing the SQL statements performed by the action that + * triggered this event. + * + * @return Guaranteed to be not {@literal null}. + * @deprecated There is currently no replacement for this. If something like this is required please create an issue + * outlining your use case. + */ + @Deprecated public AggregateChange getChange() { return change; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/support/RelationalAuditingCallback.java b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/support/RelationalAuditingCallback.java new file mode 100644 index 00000000..620db90e --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/support/RelationalAuditingCallback.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018-2019 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.domain.support; + +import lombok.RequiredArgsConstructor; + +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent; +import org.springframework.data.relational.core.mapping.event.Identifier; + +/** + * {@link BeforeConvertCallback} to capture auditing information on persisting and updating entities. + *

+ * An instance of this class gets registered when you enable auditing for Spring Data JDBC. + * + * @author Jens Schauder + */ +@RequiredArgsConstructor +public class RelationalAuditingCallback implements BeforeConvertCallback, Ordered { + + /** + * The order used for this {@link org.springframework.context.event.EventListener}. Ordering ensures that this + * {@link ApplicationListener} will run before other listeners without a specified priority. + * + * @see org.springframework.core.annotation.Order + * @see Ordered + */ + public static final int AUDITING_ORDER = 100; + + private final IsNewAwareAuditingHandler handler; + + /* + * (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return AUDITING_ORDER; + } + + @Override + public Object onBeforeConvert(Object entity, Identifier id) { + return handler.markAudited(entity); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/support/RelationalAuditingEventListener.java b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/support/RelationalAuditingEventListener.java index 82e9ca6a..a6c29c1c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/support/RelationalAuditingEventListener.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/support/RelationalAuditingEventListener.java @@ -24,13 +24,13 @@ import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent; /** * Spring JDBC event listener to capture auditing information on persisting and updating entities. - *

- * An instance of this class gets registered when you enable auditing for Spring Data JDBC. * * @author Kazuki Shimizu * @author Jens Schauder * @author Oliver Gierke + * @deprecated Use {@link RelationalAuditingCallback} instead. */ +@Deprecated @RequiredArgsConstructor public class RelationalAuditingEventListener implements ApplicationListener, Ordered { diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index ad32866e..09c61e95 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -563,9 +563,9 @@ Note that the type used for prefixing the statement name is the name of the aggr |=== [[jdbc.events]] -== Events +== Events and Callback -Spring Data JDBC triggers events that get published to any matching `ApplicationListener` in the application context. +Spring Data JDBC triggers events that get published to any matching `ApplicationListener` and `EntityCallback` beans in the application context. For example, the following listener gets invoked before an aggregate gets saved: ==== @@ -586,27 +586,34 @@ public ApplicationListener timeStampingSaveTime() { ---- ==== -The following table describes the available events: +The following table describes the available events and callbacks: .Available events |=== -| Event | When It Is Published +| Event | `EntityCallback` | When It Is Published | {javadoc-base}org/springframework/data/relational/core/mapping/event/BeforeDeleteEvent.html[`BeforeDeleteEvent`] +| {javadoc-base}org/springframework/data/relational/core/mapping/event/BeforeDeleteCallback.html[`BeforeDeleteCallback`] | Before an aggregate root gets deleted. | {javadoc-base}org/springframework/data/relational/core/mapping/event/AfterDeleteEvent.html[`AfterDeleteEvent`] +| {javadoc-base}org/springframework/data/relational/core/mapping/event/AfterDeleteCallback.html[`AfterDeleteCallback`] | After an aggregate root gets deleted. -| {javadoc-base}/org/springframework/data/relational/core/mapping/event/BeforeSaveEvent.html[`BeforeSaveEvent`] +| {javadoc-base}/org/springframework/data/relational/core/mapping/event/BeforeConvertEvent.html[`BeforeConvertEvent`] +| {javadoc-base}/org/springframework/data/relational/core/mapping/event/BeforeConvertCallback.html[`BeforeConvertCallback`] +| Before an aggregate root gets saved (that is, inserted or updated but after the decision about whether if it gets updated or deleted was made). + +| {javadoc-base}/org/springframework/data/relational/core/mapping/event/BeforeSaveEvent.html[`BeforeSaveEvent`] +| {javadoc-base}/org/springframework/data/relational/core/mapping/event/BeforeSaveCallback.html[`BeforeSaveCallback`] | Before an aggregate root gets saved (that is, inserted or updated but after the decision about whether if it gets updated or deleted was made). -The event has a reference to an {javadoc-base}/org/springframework/data/relational/core/conversion/AggregateChange.html[`AggregateChange`] instance. -The instance can be modified by adding or removing {javadoc-base}/org/springframework/data/relational/core/conversion/DbAction.html[`DbAction`] instances. | {javadoc-base}org/springframework/data/relational/core/mapping/event/AfterSaveEvent.html[`AfterSaveEvent`] +| {javadoc-base}org/springframework/data/relational/core/mapping/event/AfterSaveCallback.html[`AfterSaveCallback`] | After an aggregate root gets saved (that is, inserted or updated). | {javadoc-base}org/springframework/data/relational/core/mapping/event/AfterLoadEvent.html[`AfterLoadEvent`] +| {javadoc-base}org/springframework/data/relational/core/mapping/event/AfterLoadCallback.html[`AfterLoadCallback`] | After an aggregate root gets created from a database `ResultSet` and all its property get set. |===