DATAJDBC-393 - EntityCallback support.

An `EntityCallback` works very similar to an `ApplicationEvent` but returns a potentially changed instance.
The returned instance will be used in further processing which enables proper event handling for immutable classes.

Auditing was changed to use a callback making it work also with immutable objects.

Original pull request: #161.
This commit is contained in:
Jens Schauder
2019-07-10 12:18:55 +02:00
committed by Mark Paluch
parent d143d8fe71
commit 1a9612c64f
27 changed files with 626 additions and 97 deletions

View File

@@ -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<T> change = createChange(instance);
Function<T, AggregateChange<T>> 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<T> 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<T> 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<T> 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<T> 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> T store(T instance, IdentifierAccessor identifierAccessor, AggregateChange<T> change,
private <T> T store(T aggregateRoot, Function<T, AggregateChange<T>> 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<T> 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 <T> void deleteTree(Object id, @Nullable T entity, Class<T> domainType) {
AggregateChange<?> change = createDeletingChange(id, entity, domainType);
AggregateChange<T> change = createDeletingChange(id, entity, domainType);
Specified specifiedId = Identifier.of(id);
Optional<Object> 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 <T> AggregateChange<T> createChange(T instance) {
// context.getRequiredPersistentEntity(o.getClass()).isNew(o)
AggregateChange<T> 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 <T> AggregateChange<T> createDeletingChange(Object id, @Nullable T entity, Class<T> domainType) {
AggregateChange<?> aggregateChange = new AggregateChange(Kind.DELETE, domainType, entity);
AggregateChange<T> 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 <T> void publishAfterLoad(Iterable<T> all) {
private <T> Iterable<T> triggerAfterLoad(Iterable<T> 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> 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> T triggerBeforeConvert(T aggregateRoot, @Nullable Object id) {
Identifier identifier = Identifier.ofNullable(id);
return entityCallbacks.callback(BeforeConvertCallback.class, aggregateRoot, identifier);
}
private <T> T triggerBeforeSave(T aggregateRoot, @Nullable Object id, AggregateChange<T> change) {
Identifier identifier = Identifier.ofNullable(id);
publisher.publishEvent(new BeforeSaveEvent( //
identifier, //
aggregateRoot, //
change //
));
return entityCallbacks.callback(BeforeSaveCallback.class, aggregateRoot, identifier);
}
private <T> T triggerAfterSave(T aggregateRoot, Object id, AggregateChange<T> change) {
Specified identifier = Identifier.of(id);
publisher.publishEvent(new AfterSaveEvent( //
identifier, //
aggregateRoot, //
change //
));
return entityCallbacks.callback(AfterSaveCallback.class, aggregateRoot, identifier);
}
@Nullable
private <T> 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 <T> void publishAfterLoad(Object id, T entity) {
publisher.publishEvent(new AfterLoadEvent(Identifier.of(id), entity));
@Nullable
private <T> 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> T callback(Class<? extends EntityCallback> callbackType, T entity, Object... args) {
return entity;
}
}
}
}

View File

@@ -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);

View File

@@ -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<?, Object> 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;
}
}

View File

@@ -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<T extends Repository<S, ID>, 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<T extends Repository<S, ID>, 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<T extends Repository<S, ID>, 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<T extends Repository<S, ID>, S, ID extend
this.queryMappingConfiguration = QueryMappingConfiguration.EMPTY;
}
if (beanFactory != null) {
entityCallbacks = EntityCallbacks.create(beanFactory);
}
super.afterPropertiesSet();
}
}

View File

@@ -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<String, NoIdMapChain3> 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);
}
}
}

View File

@@ -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<SampleEntity> 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;

View File

@@ -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<PrimitiveIdEntity, Long> {}
public interface ReadOnlyIdEntityRepository extends CrudRepository<ReadOnlyIdEntity, Long> {}
private interface ImmutableWithManualIdEntityRepository extends CrudRepository<ImmutableWithManualIdEntity, Long> {}
@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<ImmutableWithManualIdEntity> idGenerator() {
return (e, __) -> e.withId(lastId.incrementAndGet());
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -8,7 +8,7 @@
</appender>
<!--<logger name="org.springframework.data" level="info" />-->
<!--<logger name="org.springframework.jdbc.core" level="trace" />-->
<logger name="org.springframework.jdbc.core" level="trace" />
<!--<logger name="org.springframework.data.jdbc.mybatis.DummyEntityMapper" level="trace" />-->
<root level="warn">

View File

@@ -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));