DATAJDBC-454 - Redesigned events.

Ids are only contained if it can not be guaranteed that an entity is contained which applies to the delete events.
As a side effect Identifier got simplified into a single simple class.

Original pull request: #199.
This commit is contained in:
Jens Schauder
2020-03-13 08:58:24 +01:00
committed by Mark Paluch
parent 7a2a3fb2eb
commit 22f9eded60
24 changed files with 262 additions and 673 deletions

View File

@@ -17,7 +17,6 @@ package org.springframework.data.jdbc.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@@ -40,7 +39,6 @@ import org.springframework.data.relational.core.conversion.RelationalEntityUpdat
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
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;
@@ -212,7 +210,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
T entity = accessStrategy.findById(id, domainType);
if (entity != null) {
return triggerAfterLoad(id, entity);
return triggerAfterLoad(entity);
}
return entity;
}
@@ -337,8 +335,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
AggregateChange<T> change = changeCreator.apply(aggregateRoot);
aggregateRoot = triggerBeforeSave(aggregateRoot,
persistentEntity.getIdentifierAccessor(aggregateRoot).getIdentifier(), change);
aggregateRoot = triggerBeforeSave(aggregateRoot, change);
change.setEntity(aggregateRoot);
@@ -348,7 +345,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
Assert.notNull(identifier, "After saving the identifier must not be null!");
return triggerAfterSave(change.getEntity(), identifier, change);
return triggerAfterSave(change.getEntity(), change);
}
private <T> void deleteTree(Object id, @Nullable T entity, Class<T> domainType) {
@@ -396,19 +393,15 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
List<T> result = new ArrayList<>();
for (T e : all) {
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(e.getClass());
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(e);
result.add(triggerAfterLoad(identifierAccessor.getRequiredIdentifier(), e));
result.add(triggerAfterLoad(e));
}
return result;
}
private <T> T triggerAfterLoad(Object id, T entity) {
private <T> T triggerAfterLoad(T entity) {
publisher.publishEvent(new AfterLoadEvent(Identifier.of(id), entity));
publisher.publishEvent(new AfterLoadEvent(entity));
return entityCallbacks.callback(AfterLoadCallback.class, entity);
}
@@ -417,33 +410,27 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
return entityCallbacks.callback(BeforeConvertCallback.class, aggregateRoot);
}
private <T> T triggerBeforeSave(T aggregateRoot, @Nullable Object id, AggregateChange<T> change) {
private <T> T triggerBeforeSave(T aggregateRoot, AggregateChange<T> change) {
publisher.publishEvent(new BeforeSaveEvent( //
Identifier.ofNullable(id), //
//
aggregateRoot, //
change //
));
//
change));
return entityCallbacks.callback(BeforeSaveCallback.class, aggregateRoot, change);
}
private <T> T triggerAfterSave(T aggregateRoot, Object id, AggregateChange<T> change) {
private <T> T triggerAfterSave(T aggregateRoot, AggregateChange<T> change) {
Specified identifier = Identifier.of(id);
publisher.publishEvent(new AfterSaveEvent( //
identifier, //
aggregateRoot, //
change //
));
publisher.publishEvent(new AfterSaveEvent(aggregateRoot, change));
return entityCallbacks.callback(AfterSaveCallback.class, aggregateRoot);
}
private <T> void triggerAfterDelete(@Nullable T aggregateRoot, Object id, AggregateChange<?> change) {
publisher.publishEvent(new AfterDeleteEvent(Identifier.of(id), Optional.ofNullable(aggregateRoot), change));
publisher.publishEvent(new AfterDeleteEvent(Identifier.of(id), aggregateRoot, change));
if (aggregateRoot != null) {
entityCallbacks.callback(AfterDeleteCallback.class, aggregateRoot);
@@ -453,7 +440,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
@Nullable
private <T> T triggerBeforeDelete(@Nullable T aggregateRoot, Object id, AggregateChange<?> change) {
publisher.publishEvent(new BeforeDeleteEvent(Identifier.of(id), Optional.ofNullable(aggregateRoot), change));
publisher.publishEvent(new BeforeDeleteEvent(Identifier.of(id), aggregateRoot, change));
if (aggregateRoot != null) {
return entityCallbacks.callback(BeforeDeleteCallback.class, aggregateRoot, change);

View File

@@ -31,7 +31,6 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.event.AfterLoadCallback;
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
import org.springframework.data.relational.core.mapping.event.Identifier;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.jdbc.core.ResultSetExtractor;
@@ -213,10 +212,8 @@ class JdbcRepositoryQuery implements RepositoryQuery {
MapSqlParameterSource parameters = new MapSqlParameterSource();
queryMethod.getParameters().getBindableParameters().forEach(p -> {
convertAndAddParameter(parameters, p, objects[p.getIndex()]);
});
queryMethod.getParameters().getBindableParameters()
.forEach(p -> convertAndAddParameter(parameters, p, objects[p.getIndex()]));
return parameters;
}
@@ -291,7 +288,7 @@ class JdbcRepositoryQuery implements RepositoryQuery {
Object identifier = e.getIdentifierAccessor(entity).getIdentifier();
if (identifier != null) {
publisher.publishEvent(new AfterLoadEvent(Identifier.of(identifier), entity));
publisher.publishEvent(new AfterLoadEvent(entity));
}
callbacks.callback(AfterLoadCallback.class, entity);

View File

@@ -1,232 +0,0 @@
/*
* Copyright 2017-2020 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.jdbc.repository;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import junit.framework.AssertionFailedError;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.util.List;
import java.util.Random;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.mapping.event.BeforeDeleteEvent;
import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.repository.CrudRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule;
/**
* Tests that the event infrastructure of Spring Data JDBC is sufficient to manipulate the {@link DbAction}s to be
* executed against the database.
*
* @author Jens Schauder
* @author Greg Turnquist
*/
@ContextConfiguration
public class JdbcRepositoryManipulateDbActionsIntegrationTests {
@ClassRule public static final SpringClassRule classRule = new SpringClassRule();
@Rule public SpringMethodRule methodRule = new SpringMethodRule();
@Autowired DummyEntityRepository repository;
@Autowired LogRepository logRepository;
@Test // DATAJDBC-120
public void softDelete() {
// given a persistent entity
DummyEntity entity = new DummyEntity(null, "Hello");
repository.save(entity);
assertThat(entity.id).isNotNull();
// when I delete the entity
repository.delete(entity);
// it is still in the repository, but marked as deleted
assertThat(repository.findById(entity.id)) //
.contains(new DummyEntity( //
entity.id, //
entity.name, //
true) //
);
}
@Test // DATAJDBC-120
public void softDeleteMany() {
// given persistent entities
DummyEntity one = new DummyEntity(null, "One");
DummyEntity two = new DummyEntity(null, "Two");
repository.saveAll(asList(one, two));
assertThat(one.id).isNotNull();
// when I delete the entities
repository.deleteAll(asList(one, two));
// they are still in the repository, but marked as deleted
assertThat(repository.findById(one.id)) //
.contains(new DummyEntity( //
one.id, //
one.name, //
true) //
);
assertThat(repository.findById(two.id)) //
.contains(new DummyEntity( //
two.id, //
two.name, //
true) //
);
}
@Test // DATAJDBC-120
public void loggingOnSave() {
// given a new entity
DummyEntity one = new DummyEntity(null, "one");
repository.save(one);
assertThat(one.id).isNotNull();
// they are still in the repository, but marked as deleted
assertThat(logRepository.findById(Config.lastLogId)) //
.isNotEmpty() //
.map(Log::getText) //
.contains("one saved");
}
@Test // DATAJDBC-120
public void loggingOnSaveMany() {
// given a new entity
DummyEntity one = new DummyEntity(null, "one");
DummyEntity two = new DummyEntity(null, "two");
repository.saveAll(asList(one, two));
assertThat(one.id).isNotNull();
// they are still in the repository, but marked as deleted
assertThat(logRepository.findById(Config.lastLogId)) //
.isNotEmpty() //
.map(Log::getText) //
.contains("two saved");
}
@Data
private static class DummyEntity {
@Id Long id;
String name;
boolean deleted;
DummyEntity(Long id, String name) {
this.id = id;
this.name = name;
this.deleted = false;
}
@PersistenceConstructor
DummyEntity(Long id, String name, boolean deleted) {
this.id = id;
this.name = name;
this.deleted = deleted;
}
}
private interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {}
@Getter
@Setter
@RequiredArgsConstructor
private static class Log {
@Id Long id;
DummyEntity entity;
String text;
}
private interface LogRepository extends CrudRepository<Log, Long> {}
@Configuration
@Import(TestConfiguration.class)
@EnableJdbcRepositories(considerNestedRepositories = true)
static class Config {
static long lastLogId;
@Bean
Class<?> testClass() {
return JdbcRepositoryManipulateDbActionsIntegrationTests.class;
}
@Bean
ApplicationListener<BeforeDeleteEvent> softDeleteListener() {
return event -> {
DummyEntity entity = (DummyEntity) event.getOptionalEntity().orElseThrow(AssertionFailedError::new);
entity.deleted = true;
List<DbAction<?>> actions = event.getChange().getActions();
actions.clear();
actions.add(new DbAction.UpdateRoot<>(entity));
};
}
@Bean
ApplicationListener<BeforeSaveEvent> logOnSaveListener() {
// this would actually be easier to implement with an AfterSaveEvent listener, but we want to test AggregateChange
// manipulation.
return event -> {
DummyEntity entity = (DummyEntity) event.getOptionalEntity().orElseThrow(AssertionFailedError::new);
lastLogId = new Random().nextLong();
Log log = new Log();
log.setId(lastLogId);
log.entity = entity;
log.text = entity.name + " saved";
List<DbAction<?>> actions = event.getChange().getActions();
actions.add(new DbAction.InsertRoot<>(log));
};
}
}
}

View File

@@ -20,7 +20,6 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import junit.framework.AssertionFailedError;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.With;
@@ -57,11 +56,13 @@ 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.RelationalEvent;
import org.springframework.data.relational.core.mapping.event.WithId;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.lang.Nullable;
/**
* Unit tests for application events via {@link SimpleJdbcRepository}.
@@ -143,14 +144,23 @@ public class SimpleJdbcRepositoryEventsUnitTests {
assertThat(publisher.events).extracting( //
RelationalEvent::getClass, //
e -> e.getOptionalEntity().orElseGet(AssertionFailedError::new), //
RelationalEvent::getId //
this::getEntity, //
this::getId //
).containsExactly( //
Tuple.tuple(BeforeDeleteEvent.class, entity, Identifier.of(23L)), //
Tuple.tuple(AfterDeleteEvent.class, entity, Identifier.of(23L)) //
);
}
private Identifier getId(RelationalEvent e) {
return ((WithId) e).getId();
}
@Nullable
private Object getEntity(RelationalEvent e) {
return e.getEntity();
}
@Test // DATAJDBC-99
@SuppressWarnings("rawtypes")
public void publishesEventsOnDeleteById() {

View File

@@ -61,8 +61,7 @@ public class JdbcRepositoryFactoryBeanUnitTests {
@Mock DataAccessStrategy dataAccessStrategy;
@Mock ApplicationEventPublisher publisher;
@Mock(answer = Answers.RETURNS_DEEP_STUBS) ListableBeanFactory beanFactory;
@Mock
Dialect dialect;
@Mock Dialect dialect;
RelationalMappingContext mappingContext;
@@ -78,10 +77,11 @@ public class JdbcRepositoryFactoryBeanUnitTests {
ObjectProvider<DataAccessStrategy> provider = mock(ObjectProvider.class);
when(beanFactory.getBeanProvider(DataAccessStrategy.class)).thenReturn(provider);
when(provider.getIfAvailable(any())).then((Answer) invocation -> ((Supplier) invocation.getArgument(0)).get());
when(provider.getIfAvailable(any()))
.then((Answer<?>) invocation -> ((Supplier<?>) invocation.getArgument(0)).get());
}
@Test
@Test // DATAJDBC-151
public void setsUpBasicInstanceCorrectly() {
factoryBean.setDataAccessStrategy(dataAccessStrategy);
@@ -95,7 +95,7 @@ public class JdbcRepositoryFactoryBeanUnitTests {
assertThat(factoryBean.getObject()).isNotNull();
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = IllegalArgumentException.class) // DATAJDBC-151
public void requiresListableBeanFactory() {
factoryBean.setBeanFactory(mock(BeanFactory.class));