diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java index a3b3f75bb..fe119dde2 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java @@ -24,85 +24,103 @@ import org.springframework.lang.Nullable; */ public interface JdbcAggregateOperations { - /** - * Saves an instance of an aggregate, including all the members of the aggregate. - * - * @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}. - * @param the type of the aggregate root. - * @return the saved instance. - */ - T save(T instance); + /** + * Saves an instance of an aggregate, including all the members of the aggregate. + * + * @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}. + * @param the type of the aggregate root. + * @return the saved instance. + */ + T save(T instance); - /** - * Deletes a single Aggregate including all entities contained in that aggregate. - * - * @param id the id of the aggregate root of the aggregate to be deleted. Must not be {@code null}. - * @param domainType the type of the aggregate root. - * @param the type of the aggregate root. - */ - void deleteById(Object id, Class domainType); + /** + * Dedicated insert function to do just the insert of an instance of an aggregate, including all the members of the aggregate. + * + * @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}. + * @param the type of the aggregate root. + * @return the saved instance. + */ + T insert(T instance); - /** - * Delete an aggregate identified by it's aggregate root. - * - * @param aggregateRoot to delete. Must not be {@code null}. - * @param domainType the type of the aggregate root. Must not be {@code null}. - * @param the type of the aggregate root. - */ - void delete(T aggregateRoot, Class domainType); + /** + * Dedicated update function to do just an update of an instance of an aggregate, including all the members of the aggregate. + * + * @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}. + * @param the type of the aggregate root. + * @return the saved instance. + */ + T update(T instance); - /** - * Delete all aggregates of a given type. - * - * @param domainType type of the aggregate roots to be deleted. Must not be {@code null}. - */ - void deleteAll(Class domainType); + /** + * Deletes a single Aggregate including all entities contained in that aggregate. + * + * @param id the id of the aggregate root of the aggregate to be deleted. Must not be {@code null}. + * @param domainType the type of the aggregate root. + * @param the type of the aggregate root. + */ + void deleteById(Object id, Class domainType); - /** - * Counts the number of aggregates of a given type. - * - * @param domainType the type of the aggregates to be counted. - * @return the number of instances stored in the database. Guaranteed to be not {@code null}. - */ - long count(Class domainType); + /** + * Delete an aggregate identified by it's aggregate root. + * + * @param aggregateRoot to delete. Must not be {@code null}. + * @param domainType the type of the aggregate root. Must not be {@code null}. + * @param the type of the aggregate root. + */ + void delete(T aggregateRoot, Class domainType); - /** - * Load an aggregate from the database. - * - * @param id the id of the aggregate to load. Must not be {@code null}. - * @param domainType the type of the aggregate root. Must not be {@code null}. - * @param the type of the aggregate root. - * @return the loaded aggregate. Might return {@code null}. - */ - @Nullable - T findById(Object id, Class domainType); + /** + * Delete all aggregates of a given type. + * + * @param domainType type of the aggregate roots to be deleted. Must not be {@code null}. + */ + void deleteAll(Class domainType); - /** - * Load all aggregates of a given type that are identified by the given ids. - * - * @param ids of the aggregate roots identifying the aggregates to load. Must not be {@code null}. - * @param domainType the type of the aggregate roots. Must not be {@code null}. - * @param the type of the aggregate roots. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. - */ - Iterable findAllById(Iterable ids, Class domainType); + /** + * Counts the number of aggregates of a given type. + * + * @param domainType the type of the aggregates to be counted. + * @return the number of instances stored in the database. Guaranteed to be not {@code null}. + */ + long count(Class domainType); - /** - * Load all aggregates of a given type. - * - * @param domainType the type of the aggregate roots. Must not be {@code null}. - * @param the type of the aggregate roots. Must not be {@code null}. - * @return Guaranteed to be not {@code null}. - */ - Iterable findAll(Class domainType); + /** + * Load an aggregate from the database. + * + * @param id the id of the aggregate to load. Must not be {@code null}. + * @param domainType the type of the aggregate root. Must not be {@code null}. + * @param the type of the aggregate root. + * @return the loaded aggregate. Might return {@code null}. + */ + @Nullable + T findById(Object id, Class domainType); - /** - * Checks if an aggregate identified by type and id exists in the database. - * - * @param id the id of the aggregate root. - * @param domainType the type of the aggregate root. - * @param the type of the aggregate root. - * @return whether the aggregate exists. - */ - boolean existsById(Object id, Class domainType); + /** + * Load all aggregates of a given type that are identified by the given ids. + * + * @param ids of the aggregate roots identifying the aggregates to load. Must not be {@code null}. + * @param domainType the type of the aggregate roots. Must not be {@code null}. + * @param the type of the aggregate roots. Must not be {@code null}. + * @return Guaranteed to be not {@code null}. + */ + Iterable findAllById(Iterable ids, Class domainType); + + /** + * Load all aggregates of a given type. + * + * @param domainType the type of the aggregate roots. Must not be {@code null}. + * @param the type of the aggregate roots. Must not be {@code null}. + * @return Guaranteed to be not {@code null}. + */ + Iterable findAll(Class domainType); + + /** + * Checks if an aggregate identified by type and id exists in the database. + * + * @param id the id of the aggregate root. + * @param domainType the type of the aggregate root. + * @param the type of the aggregate root. + * @return whether the aggregate exists. + */ + boolean existsById(Object id, Class domainType); } 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 039094de5..b60d3a7ee 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 @@ -24,6 +24,8 @@ import org.springframework.data.relational.core.conversion.AggregateChange.Kind; import org.springframework.data.relational.core.conversion.Interpreter; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter; +import org.springframework.data.relational.core.conversion.RelationalEntityInsertWriter; +import org.springframework.data.relational.core.conversion.RelationalEntityUpdateWriter; import org.springframework.data.relational.core.conversion.RelationalEntityWriter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -52,15 +54,41 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { private final RelationalEntityWriter jdbcEntityWriter; private final RelationalEntityDeleteWriter jdbcEntityDeleteWriter; + private final RelationalEntityInsertWriter jdbcEntityInsertWriter; + private final RelationalEntityUpdateWriter jdbcEntityUpdateWriter; private final DataAccessStrategy accessStrategy; + private T store(T instance, IdentifierAccessor identifierAccessor, AggregateChange change, + RelationalPersistentEntity persistentEntity) { + Assert.notNull(instance, "Aggregate instance must not be null!"); + publisher.publishEvent(new BeforeSaveEvent( // + Identifier.ofNullable(identifierAccessor.getIdentifier()), // + instance, // + change // + )); + + change.executeWith(interpreter, context, converter); + + Object identifier = persistentEntity.getIdentifierAccessor(change.getEntity()).getIdentifier(); + + 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(); + } + /** * Creates a new {@link JdbcAggregateTemplate} given {@link ApplicationEventPublisher}, * {@link RelationalMappingContext} and {@link DataAccessStrategy}. * - * @param publisher must not be {@literal null}. - * @param context must not be {@literal null}. + * @param publisher must not be {@literal null}. + * @param context must not be {@literal null}. * @param dataAccessStrategy must not be {@literal null}. */ public JdbcAggregateTemplate(ApplicationEventPublisher publisher, RelationalMappingContext context, @@ -77,6 +105,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { this.accessStrategy = dataAccessStrategy; this.jdbcEntityWriter = new RelationalEntityWriter(context); + this.jdbcEntityInsertWriter = new RelationalEntityInsertWriter(context); + this.jdbcEntityUpdateWriter = new RelationalEntityUpdateWriter(context); this.jdbcEntityDeleteWriter = new RelationalEntityDeleteWriter(context); this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy); } @@ -85,8 +115,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#save(java.lang.Object) */ - @Override - public T save(T instance) { + @Override public T save(T instance) { Assert.notNull(instance, "Aggregate instance must not be null!"); @@ -95,33 +124,48 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { AggregateChange change = createChange(instance); - publisher.publishEvent(new BeforeSaveEvent( // - Identifier.ofNullable(identifierAccessor.getIdentifier()), // - instance, // - change // - )); + return store(instance, identifierAccessor, change, persistentEntity); + } - change.executeWith(interpreter, context, converter); + /** + * Dedicated insert function to do just the insert of an instance of an aggregate, including all the members of the aggregate. + * + * @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}. + * @return the saved instance. + */ + @Override public T insert(T instance) { + Assert.notNull(instance, "Aggregate instance must not be null!"); - Object identifier = persistentEntity.getIdentifierAccessor(change.getEntity()).getIdentifier(); + RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); + IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(instance); - Assert.notNull(identifier, "After saving the identifier must not be null"); + AggregateChange change = createInsertChange(instance); - publisher.publishEvent(new AfterSaveEvent( // - Identifier.of(identifier), // - change.getEntity(), // - change // - )); + return store(instance, identifierAccessor, change, persistentEntity); + } - return (T) change.getEntity(); + /** + * Dedicated update function to do just an update of an instance of an aggregate, including all the members of the aggregate. + * + * @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}. + * @return the saved instance. + */ + @Override public T update(T instance) { + 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); } /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#count(java.lang.Class) */ - @Override - public long count(Class domainType) { + @Override public long count(Class domainType) { return accessStrategy.count(domainType); } @@ -129,8 +173,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findById(java.lang.Object, java.lang.Class) */ - @Override - public T findById(Object id, Class domainType) { + @Override public T findById(Object id, Class domainType) { T entity = accessStrategy.findById(id, domainType); if (entity != null) { @@ -143,8 +186,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#existsById(java.lang.Object, java.lang.Class) */ - @Override - public boolean existsById(Object id, Class domainType) { + @Override public boolean existsById(Object id, Class domainType) { return accessStrategy.existsById(id, domainType); } @@ -152,8 +194,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAll(java.lang.Class) */ - @Override - public Iterable findAll(Class domainType) { + @Override public Iterable findAll(Class domainType) { Iterable all = accessStrategy.findAll(domainType); publishAfterLoad(all); @@ -164,8 +205,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAllById(java.lang.Iterable, java.lang.Class) */ - @Override - public Iterable findAllById(Iterable ids, Class domainType) { + @Override public Iterable findAllById(Iterable ids, Class domainType) { Iterable allById = accessStrategy.findAllById(ids, domainType); publishAfterLoad(allById); @@ -176,8 +216,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#delete(java.lang.Object, java.lang.Class) */ - @Override - public void delete(S aggregateRoot, Class domainType) { + @Override public void delete(S aggregateRoot, Class domainType) { IdentifierAccessor identifierAccessor = context.getRequiredPersistentEntity(domainType) .getIdentifierAccessor(aggregateRoot); @@ -189,8 +228,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#deleteById(java.lang.Object, java.lang.Class) */ - @Override - public void deleteById(Object id, Class domainType) { + @Override public void deleteById(Object id, Class domainType) { deleteTree(id, null, domainType); } @@ -198,8 +236,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#deleteAll(java.lang.Class) */ - @Override - public void deleteAll(Class domainType) { + @Override public void deleteAll(Class domainType) { AggregateChange change = createDeletingChange(domainType); change.executeWith(interpreter, context, converter); @@ -218,14 +255,27 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { publisher.publishEvent(new AfterDeleteEvent(specifiedId, optionalEntity, change)); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private AggregateChange createChange(T instance) { + @SuppressWarnings({ "unchecked", "rawtypes" }) private AggregateChange createChange(T instance) { AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, instance.getClass(), instance); jdbcEntityWriter.write(instance, aggregateChange); return aggregateChange; } + @SuppressWarnings({ "unchecked", "rawtypes" }) private AggregateChange createInsertChange(T instance) { + + AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, instance.getClass(), instance); + jdbcEntityInsertWriter.write(instance, aggregateChange); + return aggregateChange; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) private AggregateChange createUpdateChange(T instance) { + + AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, instance.getClass(), instance); + jdbcEntityUpdateWriter.write(instance, aggregateChange); + return aggregateChange; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) private AggregateChange createDeletingChange(Object id, @Nullable Object entity, Class domainType) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcRepository.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcRepository.java new file mode 100644 index 000000000..beaae03c3 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcRepository.java @@ -0,0 +1,18 @@ +package org.springframework.data.jdbc.core; + +import org.springframework.data.repository.Repository; + +/** + * Jdbc repository for dedicated insert(), update() and upsert() sql functions. + * Other than {@link org.springframework.data.jdbc.repository.support.SimpleJdbcRepository} + * there should be bypassing of the isNew check. + * + * @author Thomas Lang + * @see DATAJDBC-282 + */ +public interface JdbcRepository extends Repository { + + S insert(S var1); + + S update(S var1); +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java index 9ff5581f7..b6b846d22 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.stream.Collectors; import org.springframework.data.jdbc.core.JdbcAggregateOperations; +import org.springframework.data.jdbc.core.JdbcRepository; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.repository.CrudRepository; import org.springframework.data.util.Streamable; @@ -31,107 +32,127 @@ import org.springframework.data.util.Streamable; * @author Oliver Gierke */ @RequiredArgsConstructor -public class SimpleJdbcRepository implements CrudRepository { - - private final @NonNull JdbcAggregateOperations entityOperations; - private final @NonNull PersistentEntity entity; - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#save(S) - */ - @Override - public S save(S instance) { - return entityOperations.save(instance); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) - */ - @Override - public Iterable saveAll(Iterable entities) { - - return Streamable.of(entities).stream() // - .map(this::save) // - .collect(Collectors.toList()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) - */ - @Override - public Optional findById(ID id) { - return Optional.ofNullable(entityOperations.findById(id, entity.getType())); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable) - */ - @Override - public boolean existsById(ID id) { - return entityOperations.existsById(id, entity.getType()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#findAll() - */ - @Override - public Iterable findAll() { - return entityOperations.findAll(entity.getType()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) - */ - @Override - public Iterable findAllById(Iterable ids) { - return entityOperations.findAllById(ids, entity.getType()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#count() - */ - @Override - public long count() { - return entityOperations.count(entity.getType()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) - */ - @Override - public void deleteById(ID id) { - entityOperations.deleteById(id, entity.getType()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) - */ - @Override - public void delete(T instance) { - entityOperations.delete(instance, entity.getType()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable) - */ - @Override - @SuppressWarnings("unchecked") - public void deleteAll(Iterable entities) { - entities.forEach(it -> entityOperations.delete(it, (Class) it.getClass())); - } - - @Override - public void deleteAll() { - entityOperations.deleteAll(entity.getType()); - } +public class SimpleJdbcRepository implements CrudRepository, JdbcRepository { + + private final @NonNull + JdbcAggregateOperations entityOperations; + private final @NonNull + PersistentEntity entity; + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#save(S) + */ + @Override + public S save(S instance) { + return entityOperations.save(instance); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) + */ + @Override + public Iterable saveAll(Iterable entities) { + + return Streamable.of(entities).stream() // + .map(this::save) // + .collect(Collectors.toList()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) + */ + @Override + public Optional findById(ID id) { + return Optional.ofNullable(entityOperations.findById(id, entity.getType())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable) + */ + @Override + public boolean existsById(ID id) { + return entityOperations.existsById(id, entity.getType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#findAll() + */ + @Override + public Iterable findAll() { + return entityOperations.findAll(entity.getType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) + */ + @Override + public Iterable findAllById(Iterable ids) { + return entityOperations.findAllById(ids, entity.getType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#count() + */ + @Override + public long count() { + return entityOperations.count(entity.getType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) + */ + @Override + public void deleteById(ID id) { + entityOperations.deleteById(id, entity.getType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) + */ + @Override + public void delete(T instance) { + entityOperations.delete(instance, entity.getType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable) + */ + @Override + @SuppressWarnings("unchecked") + public void deleteAll(Iterable entities) { + entities.forEach(it -> entityOperations.delete(it, (Class) it.getClass())); + } + + @Override + public void deleteAll() { + entityOperations.deleteAll(entity.getType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.JdbcRepository#insert(T t) + */ + @Override + public S insert(S var1) { + return entityOperations.insert(var1); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.JdbcRepository#update(T t) + */ + @Override + public S update(S var1) { + return entityOperations.update(var1); + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 0ab12f9cb..d30692637 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -28,6 +28,7 @@ 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.jdbc.core.JdbcRepository; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.repository.CrudRepository; @@ -48,213 +49,246 @@ import org.springframework.transaction.annotation.Transactional; @Transactional public class JdbcRepositoryIntegrationTests { - @Configuration - @Import(TestConfiguration.class) - static class Config { + @Configuration + @Import(TestConfiguration.class) + static class Config { - @Autowired JdbcRepositoryFactory factory; + @Autowired + JdbcRepositoryFactory factory; - @Bean - Class testClass() { - return JdbcRepositoryIntegrationTests.class; - } + @Bean + Class testClass() { + return JdbcRepositoryIntegrationTests.class; + } - @Bean - DummyEntityRepository dummyEntityRepository() { - return factory.getRepository(DummyEntityRepository.class); - } + @Bean + DummyEntityRepository dummyEntityRepository() { + return factory.getRepository(DummyEntityRepository.class); + } - } + } - @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); - @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + @ClassRule + public static final SpringClassRule classRule = new SpringClassRule(); + @Rule + public SpringMethodRule methodRule = new SpringMethodRule(); - @Autowired NamedParameterJdbcTemplate template; - @Autowired DummyEntityRepository repository; + @Autowired + NamedParameterJdbcTemplate template; + @Autowired + DummyEntityRepository repository; - @Test // DATAJDBC-95 - public void savesAnEntity() { + @Test // DATAJDBC-95 + public void savesAnEntity() { - DummyEntity entity = repository.save(createDummyEntity()); + DummyEntity entity = repository.save(createDummyEntity()); - assertThat(JdbcTestUtils.countRowsInTableWhere((JdbcTemplate) template.getJdbcOperations(), "dummy_entity", - "id_Prop = " + entity.getIdProp())).isEqualTo(1); - } + assertThat(JdbcTestUtils.countRowsInTableWhere((JdbcTemplate) template.getJdbcOperations(), "dummy_entity", + "id_Prop = " + entity.getIdProp())).isEqualTo(1); + } - @Test // DATAJDBC-95 - public void saveAndLoadAnEntity() { + @Test // DATAJDBC-282 + public void insertAnEntity() { - DummyEntity entity = repository.save(createDummyEntity()); + DummyEntity entity = repository.insert(createDummyEntity()); - assertThat(repository.findById(entity.getIdProp())).hasValueSatisfying(it -> { + assertThat(JdbcTestUtils.countRowsInTableWhere((JdbcTemplate) template.getJdbcOperations(), "dummy_entity", + "id_Prop = " + entity.getIdProp())).isEqualTo(1); + } - assertThat(it.getIdProp()).isEqualTo(entity.getIdProp()); - assertThat(it.getName()).isEqualTo(entity.getName()); - }); - } + @Test // DATAJDBC-282 + public void insertAnExistingEntity() { - @Test // DATAJDBC-97 - public void savesManyEntities() { + DummyEntity existingDummyEntity = createExistingDummyEntity(); + DummyEntity entity = repository.insert(existingDummyEntity); + assertThat(JdbcTestUtils.countRowsInTableWhere((JdbcTemplate) template.getJdbcOperations(), "dummy_entity", + "id_Prop = " + existingDummyEntity.getIdProp())).isEqualTo(1); + } - DummyEntity entity = createDummyEntity(); - DummyEntity other = createDummyEntity(); + @Test // DATAJDBC-95 + public void saveAndLoadAnEntity() { - repository.saveAll(asList(entity, other)); + DummyEntity entity = repository.save(createDummyEntity()); - assertThat(repository.findAll()) // - .extracting(DummyEntity::getIdProp) // - .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); - } + assertThat(repository.findById(entity.getIdProp())).hasValueSatisfying(it -> { - @Test // DATAJDBC-97 - public void existsReturnsTrueIffEntityExists() { + assertThat(it.getIdProp()).isEqualTo(entity.getIdProp()); + assertThat(it.getName()).isEqualTo(entity.getName()); + }); + } - DummyEntity entity = repository.save(createDummyEntity()); + @Test // DATAJDBC-97 + public void savesManyEntities() { - assertThat(repository.existsById(entity.getIdProp())).isTrue(); - assertThat(repository.existsById(entity.getIdProp() + 1)).isFalse(); - } + DummyEntity entity = createDummyEntity(); + DummyEntity other = createDummyEntity(); - @Test // DATAJDBC-97 - public void findAllFindsAllEntities() { + repository.saveAll(asList(entity, other)); - DummyEntity entity = repository.save(createDummyEntity()); - DummyEntity other = repository.save(createDummyEntity()); + assertThat(repository.findAll()) // + .extracting(DummyEntity::getIdProp) // + .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); + } - Iterable all = repository.findAll(); + @Test // DATAJDBC-97 + public void existsReturnsTrueIffEntityExists() { - assertThat(all)// - .extracting(DummyEntity::getIdProp)// - .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); - } + DummyEntity entity = repository.save(createDummyEntity()); - @Test // DATAJDBC-97 - public void findAllFindsAllSpecifiedEntities() { + assertThat(repository.existsById(entity.getIdProp())).isTrue(); + assertThat(repository.existsById(entity.getIdProp() + 1)).isFalse(); + } - DummyEntity entity = repository.save(createDummyEntity()); - DummyEntity other = repository.save(createDummyEntity()); + @Test // DATAJDBC-97 + public void findAllFindsAllEntities() { - assertThat(repository.findAllById(asList(entity.getIdProp(), other.getIdProp())))// - .extracting(DummyEntity::getIdProp)// - .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); - } + DummyEntity entity = repository.save(createDummyEntity()); + DummyEntity other = repository.save(createDummyEntity()); - @Test // DATAJDBC-97 - public void countsEntities() { + Iterable all = repository.findAll(); - repository.save(createDummyEntity()); - repository.save(createDummyEntity()); - repository.save(createDummyEntity()); + assertThat(all)// + .extracting(DummyEntity::getIdProp)// + .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); + } - assertThat(repository.count()).isEqualTo(3L); - } + @Test // DATAJDBC-97 + public void findAllFindsAllSpecifiedEntities() { - @Test // DATAJDBC-97 - public void deleteById() { + DummyEntity entity = repository.save(createDummyEntity()); + DummyEntity other = repository.save(createDummyEntity()); - DummyEntity one = repository.save(createDummyEntity()); - DummyEntity two = repository.save(createDummyEntity()); - DummyEntity three = repository.save(createDummyEntity()); + assertThat(repository.findAllById(asList(entity.getIdProp(), other.getIdProp())))// + .extracting(DummyEntity::getIdProp)// + .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); + } - repository.deleteById(two.getIdProp()); + @Test // DATAJDBC-97 + public void countsEntities() { - assertThat(repository.findAll()) // - .extracting(DummyEntity::getIdProp) // - .containsExactlyInAnyOrder(one.getIdProp(), three.getIdProp()); - } + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); - @Test // DATAJDBC-97 - public void deleteByEntity() { + assertThat(repository.count()).isEqualTo(3L); + } - DummyEntity one = repository.save(createDummyEntity()); - DummyEntity two = repository.save(createDummyEntity()); - DummyEntity three = repository.save(createDummyEntity()); + @Test // DATAJDBC-97 + public void deleteById() { - repository.delete(one); + DummyEntity one = repository.save(createDummyEntity()); + DummyEntity two = repository.save(createDummyEntity()); + DummyEntity three = repository.save(createDummyEntity()); - assertThat(repository.findAll()) // - .extracting(DummyEntity::getIdProp) // - .containsExactlyInAnyOrder(two.getIdProp(), three.getIdProp()); - } + repository.deleteById(two.getIdProp()); - @Test // DATAJDBC-97 - public void deleteByList() { + assertThat(repository.findAll()) // + .extracting(DummyEntity::getIdProp) // + .containsExactlyInAnyOrder(one.getIdProp(), three.getIdProp()); + } - DummyEntity one = repository.save(createDummyEntity()); - DummyEntity two = repository.save(createDummyEntity()); - DummyEntity three = repository.save(createDummyEntity()); + @Test // DATAJDBC-97 + public void deleteByEntity() { - repository.deleteAll(asList(one, three)); + DummyEntity one = repository.save(createDummyEntity()); + DummyEntity two = repository.save(createDummyEntity()); + DummyEntity three = repository.save(createDummyEntity()); - assertThat(repository.findAll()) // - .extracting(DummyEntity::getIdProp) // - .containsExactlyInAnyOrder(two.getIdProp()); - } + repository.delete(one); - @Test // DATAJDBC-97 - public void deleteAll() { + assertThat(repository.findAll()) // + .extracting(DummyEntity::getIdProp) // + .containsExactlyInAnyOrder(two.getIdProp(), three.getIdProp()); + } - repository.save(createDummyEntity()); - repository.save(createDummyEntity()); - repository.save(createDummyEntity()); + @Test // DATAJDBC-97 + public void deleteByList() { - assertThat(repository.findAll()).isNotEmpty(); + DummyEntity one = repository.save(createDummyEntity()); + DummyEntity two = repository.save(createDummyEntity()); + DummyEntity three = repository.save(createDummyEntity()); - repository.deleteAll(); + repository.deleteAll(asList(one, three)); - assertThat(repository.findAll()).isEmpty(); - } + assertThat(repository.findAll()) // + .extracting(DummyEntity::getIdProp) // + .containsExactlyInAnyOrder(two.getIdProp()); + } - @Test // DATAJDBC-98 - public void update() { + @Test // DATAJDBC-97 + public void deleteAll() { - DummyEntity entity = repository.save(createDummyEntity()); + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); - entity.setName("something else"); - DummyEntity saved = repository.save(entity); + assertThat(repository.findAll()).isNotEmpty(); - assertThat(repository.findById(entity.getIdProp())).hasValueSatisfying(it -> { - assertThat(it.getName()).isEqualTo(saved.getName()); - }); - } + repository.deleteAll(); - @Test // DATAJDBC-98 - public void updateMany() { + assertThat(repository.findAll()).isEmpty(); + } - DummyEntity entity = repository.save(createDummyEntity()); - DummyEntity other = repository.save(createDummyEntity()); + @Test // DATAJDBC-98 + public void update() { - entity.setName("something else"); - other.setName("others Name"); + DummyEntity entity = repository.save(createDummyEntity()); - repository.saveAll(asList(entity, other)); + entity.setName("something else"); + DummyEntity saved = repository.save(entity); - assertThat(repository.findAll()) // - .extracting(DummyEntity::getName) // - .containsExactlyInAnyOrder(entity.getName(), other.getName()); - } + assertThat(repository.findById(entity.getIdProp())).hasValueSatisfying(it -> { + assertThat(it.getName()).isEqualTo(saved.getName()); + }); + } - @Test // DATAJDBC-112 - public void findByIdReturnsEmptyWhenNoneFound() { + @Test // DATAJDBC-98 + public void updateMany() { - // NOT saving anything, so DB is empty + DummyEntity entity = repository.save(createDummyEntity()); + DummyEntity other = repository.save(createDummyEntity()); - assertThat(repository.findById(-1L)).isEmpty(); - } + entity.setName("something else"); + other.setName("others Name"); - private static DummyEntity createDummyEntity() { + repository.saveAll(asList(entity, other)); - DummyEntity entity = new DummyEntity(); - entity.setName("Entity Name"); - return entity; - } + assertThat(repository.findAll()) // + .extracting(DummyEntity::getName) // + .containsExactlyInAnyOrder(entity.getName(), other.getName()); + } - interface DummyEntityRepository extends CrudRepository {} + @Test // DATAJDBC-112 + public void findByIdReturnsEmptyWhenNoneFound() { - @Data - static class DummyEntity { + // NOT saving anything, so DB is empty - String name; - @Id private Long idProp; - } + assertThat(repository.findById(-1L)).isEmpty(); + } + + private static DummyEntity createDummyEntity() { + + DummyEntity entity = new DummyEntity(); + entity.setName("Entity Name"); + return entity; + } + + private static DummyEntity createExistingDummyEntity() { + + DummyEntity entity = new DummyEntity(); + entity.setIdProp(Long.parseLong("123")); + entity.setName("Entity Name"); + return entity; + } + + interface DummyEntityRepository extends CrudRepository, JdbcRepository { + } + + @Data + static class DummyEntity { + + String name; + @Id + private Long idProp; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AbstractRelationalEntityWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AbstractRelationalEntityWriter.java new file mode 100644 index 000000000..f6b23bffd --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AbstractRelationalEntityWriter.java @@ -0,0 +1,274 @@ +package org.springframework.data.relational.core.conversion; + +import lombok.Value; +import org.springframework.data.convert.EntityWriter; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PersistentPropertyPaths; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.util.Pair; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Used as an abstract class to derive relational writer actions (save, insert, update). + * Implementations see {@link RelationalEntityWriter} and {@link RelationalEntityInsertWriter} + * + * @author Thomas Lang + */ +abstract class AbstractRelationalEntityWriter implements EntityWriter> { + + protected final RelationalMappingContext context; + + AbstractRelationalEntityWriter(RelationalMappingContext context) { + this.context = context; + } + + /** + * Holds context information for the current save operation. + */ + class WritingContext { + + private final Object root; + private final Object entity; + private final Class entityType; + private final PersistentPropertyPaths paths; + private final Map previousActions = new HashMap<>(); + private Map, List> nodesCache = new HashMap<>(); + + WritingContext(Object root, AggregateChange aggregateChange) { + + this.root = root; + this.entity = aggregateChange.getEntity(); + this.entityType = aggregateChange.getEntityType(); + this.paths = context.findPersistentPropertyPaths(entityType, PersistentProperty::isEntity); + } + + /** + * Leaves out the isNew check as defined in #DATAJDBC-282 + * + * @return List of {@link DbAction}s + * @see DAJDBC-282 + */ + List> insert() { + + List> actions = new ArrayList<>(); + actions.add(setRootAction(new DbAction.InsertRoot<>(entity))); + actions.addAll(insertReferenced()); + return actions; + } + + /** + * Leaves out the isNew check as defined in #DATAJDBC-282 + * + * @return List of {@link DbAction}s + * @see DAJDBC-282 + */ + List> update() { + + List> actions = new ArrayList<>(deleteReferenced()); + actions.add(setRootAction(new DbAction.UpdateRoot<>(entity))); + actions.addAll(insertReferenced()); + return actions; + } + + List> save() { + + List> actions = new ArrayList<>(); + if (isNew(root)) { + + actions.add(setRootAction(new DbAction.InsertRoot<>(entity))); + actions.addAll(insertReferenced()); + } else { + + actions.addAll(deleteReferenced()); + actions.add(setRootAction(new DbAction.UpdateRoot<>(entity))); + actions.addAll(insertReferenced()); + } + + return actions; + } + + private boolean isNew(Object o) { + return context.getRequiredPersistentEntity(o.getClass()).isNew(o); + } + + //// Operations on all paths + + private List> insertReferenced() { + + List> actions = new ArrayList<>(); + + paths.forEach(path -> actions.addAll(insertAll(path))); + + return actions; + } + + private List> insertAll(PersistentPropertyPath path) { + + List> actions = new ArrayList<>(); + + from(path).forEach(node -> { + + DbAction.Insert insert; + if (node.path.getRequiredLeafProperty().isQualified()) { + + Pair value = (Pair) node.getValue(); + insert = new DbAction.Insert<>(value.getSecond(), path, getAction(node.parent)); + insert.getAdditionalValues().put(node.path.getRequiredLeafProperty().getKeyColumn(), value.getFirst()); + + } else { + insert = new DbAction.Insert<>(node.getValue(), path, getAction(node.parent)); + } + + previousActions.put(node, insert); + actions.add(insert); + }); + + return actions; + } + + private List> deleteReferenced() { + + List> deletes = new ArrayList<>(); + paths.forEach(path -> deletes.add(0, deleteReferenced(path))); + + return deletes; + } + + /// Operations on a single path + + private DbAction.Delete deleteReferenced(PersistentPropertyPath path) { + + Object id = context.getRequiredPersistentEntity(entityType).getIdentifierAccessor(entity).getIdentifier(); + + return new DbAction.Delete<>(id, path); + } + + //// methods not directly related to the creation of DbActions + + private DbAction setRootAction(DbAction dbAction) { + + previousActions.put(null, dbAction); + return dbAction; + } + + @Nullable + private DbAction.WithEntity getAction(@Nullable RelationalEntityInsertWriter.PathNode parent) { + + DbAction action = previousActions.get(parent); + + if (action != null) { + + Assert.isInstanceOf( // + DbAction.WithEntity.class, // + action, // + "dependsOn action is not a WithEntity, but " + action.getClass().getSimpleName() // + ); + + return (DbAction.WithEntity) action; + } + + return null; + } + // commented as of #DATAJDBC-282 + // private boolean isNew(Object o) { + // return context.getRequiredPersistentEntity(o.getClass()).isNew(o); + // } + + private List from( + PersistentPropertyPath path) { + + List nodes = new ArrayList<>(); + + if (path.getLength() == 1) { + + Object value = context // + .getRequiredPersistentEntity(entityType) // + .getPropertyAccessor(entity) // + .getProperty(path.getRequiredLeafProperty()); + + nodes.addAll(createNodes(path, null, value)); + + } else { + + List pathNodes = nodesCache.get(path.getParentPath()); + pathNodes.forEach(parentNode -> { + + Object value = path.getRequiredLeafProperty().getOwner().getPropertyAccessor(parentNode.getValue()) + .getProperty(path.getRequiredLeafProperty()); + + nodes.addAll(createNodes(path, parentNode, value)); + }); + } + + nodesCache.put(path, nodes); + + return nodes; + } + + private List createNodes( + PersistentPropertyPath path, + @Nullable RelationalEntityInsertWriter.PathNode parentNode, @Nullable Object value) { + + if (value == null) { + return Collections.emptyList(); + } + + List nodes = new ArrayList<>(); + + if (path.getRequiredLeafProperty().isQualified()) { + + if (path.getRequiredLeafProperty().isMap()) { + ((Map) value) + .forEach((k, v) -> nodes.add(new RelationalEntityInsertWriter.PathNode(path, parentNode, Pair.of(k, v)))); + } else { + + List listValue = (List) value; + for (int k = 0; k < listValue.size(); k++) { + nodes.add(new RelationalEntityInsertWriter.PathNode(path, parentNode, Pair.of(k, listValue.get(k)))); + } + } + } else if (path.getRequiredLeafProperty().isCollectionLike()) { // collection value + ((Collection) value).forEach(v -> nodes.add(new RelationalEntityInsertWriter.PathNode(path, parentNode, v))); + } else { // single entity value + nodes.add(new RelationalEntityInsertWriter.PathNode(path, parentNode, value)); + } + + return nodes; + } + + } + + /** + * Represents a single entity in an aggregate along with its property path from the root entity and the chain of + * objects to traverse a long this path. + */ + @Value + static class PathNode { + + /** + * The path to this entity + */ + PersistentPropertyPath path; + + /** + * The parent {@link PathNode}. This is {@code null} if this is the root entity. + */ + @Nullable + PathNode parent; + + /** + * The value of the entity. + */ + Object value; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriter.java new file mode 100644 index 000000000..a283db8a9 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2018 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 + * + * http://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.conversion; + +import org.springframework.data.relational.core.mapping.RelationalMappingContext; + +import java.util.List; + +/** + * Converts an aggregate represented by its root into an {@link AggregateChange}. + * Does not perform any isNew check. + * + * @author Jens Schauder + * @author Mark Paluch + * @author Thomas Lang + */ +public class RelationalEntityInsertWriter extends AbstractRelationalEntityWriter { + + public RelationalEntityInsertWriter(RelationalMappingContext context) { + super(context); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.EntityWriter#save(java.lang.Object, java.lang.Object) + */ + @Override public void write(Object root, AggregateChange aggregateChange) { + List> actions = new WritingContext(root, aggregateChange).insert(); + actions.forEach(aggregateChange::addAction); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriter.java new file mode 100644 index 000000000..51043a1a9 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2018 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 + * + * http://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.conversion; + +import org.springframework.data.relational.core.mapping.RelationalMappingContext; + +import java.util.List; + +/** + * Converts an aggregate represented by its root into an {@link AggregateChange}. + * Does not perform any isNew check. + * + * @author Jens Schauder + * @author Mark Paluch + * @author Thomas Lang + */ +public class RelationalEntityUpdateWriter extends AbstractRelationalEntityWriter { + + public RelationalEntityUpdateWriter(RelationalMappingContext context) { + super(context); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.EntityWriter#save(java.lang.Object, java.lang.Object) + */ + @Override public void write(Object root, AggregateChange aggregateChange) { + List> actions = new WritingContext(root, aggregateChange).update(); + actions.forEach(aggregateChange::addAction); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java index 380c39c4b..0250295b9 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java @@ -15,24 +15,9 @@ */ package org.springframework.data.relational.core.conversion; -import lombok.Value; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; - -import org.springframework.data.convert.EntityWriter; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyPath; -import org.springframework.data.mapping.PersistentPropertyPaths; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; -import org.springframework.data.util.Pair; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** * Converts an aggregate represented by its root into an {@link AggregateChange}. @@ -40,223 +25,18 @@ import org.springframework.util.Assert; * @author Jens Schauder * @author Mark Paluch */ -public class RelationalEntityWriter implements EntityWriter> { - - private final RelationalMappingContext context; +public class RelationalEntityWriter extends AbstractRelationalEntityWriter { public RelationalEntityWriter(RelationalMappingContext context) { - this.context = context; + super(context); } - /* + /* * (non-Javadoc) - * @see org.springframework.data.convert.EntityWriter#write(java.lang.Object, java.lang.Object) + * @see org.springframework.data.convert.EntityWriter#save(java.lang.Object, java.lang.Object) */ - @Override - public void write(Object root, AggregateChange aggregateChange) { - - List> actions = new WritingContext(root, aggregateChange).write(); - + @Override public void write(Object root, AggregateChange aggregateChange) { + List> actions = new WritingContext(root, aggregateChange).save(); actions.forEach(aggregateChange::addAction); } - - /** - * Holds context information for the current write operation. - */ - private class WritingContext { - - private final Object root; - private final Object entity; - private final Class entityType; - private final PersistentPropertyPaths paths; - private final Map previousActions = new HashMap<>(); - private Map, List> nodesCache = new HashMap<>(); - - WritingContext(Object root, AggregateChange aggregateChange) { - - this.root = root; - this.entity = aggregateChange.getEntity(); - this.entityType = aggregateChange.getEntityType(); - this.paths = context.findPersistentPropertyPaths(entityType, PersistentProperty::isEntity); - } - - private List> write() { - - List> actions = new ArrayList<>(); - if (isNew(root)) { - - actions.add(setRootAction(new DbAction.InsertRoot<>(entity))); - actions.addAll(insertReferenced()); - } else { - - actions.addAll(deleteReferenced()); - actions.add(setRootAction(new DbAction.UpdateRoot<>(entity))); - actions.addAll(insertReferenced()); - } - - return actions; - } - - //// Operations on all paths - - private List> insertReferenced() { - - List> actions = new ArrayList<>(); - - paths.forEach(path -> actions.addAll(insertAll(path))); - - return actions; - } - - private List> insertAll(PersistentPropertyPath path) { - - List> actions = new ArrayList<>(); - - from(path).forEach(node -> { - - DbAction.Insert insert; - if (node.path.getRequiredLeafProperty().isQualified()) { - - Pair value = (Pair) node.getValue(); - insert = new DbAction.Insert<>(value.getSecond(), path, getAction(node.parent)); - insert.getAdditionalValues().put(node.path.getRequiredLeafProperty().getKeyColumn(), value.getFirst()); - - } else { - insert = new DbAction.Insert<>(node.getValue(), path, getAction(node.parent)); - } - - previousActions.put(node, insert); - actions.add(insert); - }); - - return actions; - } - - private List> deleteReferenced() { - - List> deletes = new ArrayList<>(); - paths.forEach(path -> deletes.add(0, deleteReferenced(path))); - - return deletes; - } - - /// Operations on a single path - - private DbAction.Delete deleteReferenced(PersistentPropertyPath path) { - - Object id = context.getRequiredPersistentEntity(entityType).getIdentifierAccessor(entity).getIdentifier(); - - return new DbAction.Delete<>(id, path); - } - - //// methods not directly related to the creation of DbActions - - private DbAction setRootAction(DbAction dbAction) { - - previousActions.put(null, dbAction); - return dbAction; - } - - @Nullable - private DbAction.WithEntity getAction(@Nullable PathNode parent) { - - DbAction action = previousActions.get(parent); - - if (action != null) { - - Assert.isInstanceOf( // - DbAction.WithEntity.class, // - action, // - "dependsOn action is not a WithEntity, but " + action.getClass().getSimpleName() // - ); - - return (DbAction.WithEntity) action; - } - - return null; - } - - private boolean isNew(Object o) { - return context.getRequiredPersistentEntity(o.getClass()).isNew(o); - } - - private List from(PersistentPropertyPath path) { - - List nodes = new ArrayList<>(); - - if (path.getLength() == 1) { - - Object value = context // - .getRequiredPersistentEntity(entityType) // - .getPropertyAccessor(entity) // - .getProperty(path.getRequiredLeafProperty()); - - nodes.addAll(createNodes(path, null, value)); - - } else { - - List pathNodes = nodesCache.get(path.getParentPath()); - pathNodes.forEach(parentNode -> { - - Object value = path.getRequiredLeafProperty().getOwner().getPropertyAccessor(parentNode.getValue()) - .getProperty(path.getRequiredLeafProperty()); - - nodes.addAll(createNodes(path, parentNode, value)); - }); - } - - nodesCache.put(path, nodes); - - return nodes; - } - - private List createNodes(PersistentPropertyPath path, - @Nullable PathNode parentNode, @Nullable Object value) { - - if (value == null) { - return Collections.emptyList(); - } - - List nodes = new ArrayList<>(); - - if (path.getRequiredLeafProperty().isQualified()) { - - if (path.getRequiredLeafProperty().isMap()) { - ((Map) value).forEach((k, v) -> nodes.add(new PathNode(path, parentNode, Pair.of(k, v)))); - } else { - - List listValue = (List) value; - for (int k = 0; k < listValue.size(); k++) { - nodes.add(new PathNode(path, parentNode, Pair.of(k, listValue.get(k)))); - } - } - } else if (path.getRequiredLeafProperty().isCollectionLike()) { // collection value - ((Collection) value).forEach(v -> nodes.add(new PathNode(path, parentNode, v))); - } else { // single entity value - nodes.add(new PathNode(path, parentNode, value)); - } - - return nodes; - } - - } - - /** - * Represents a single entity in an aggregate along with its property path from the root entity and the chain of - * objects to traverse a long this path. - */ - @Value - static class PathNode { - - /** The path to this entity */ - PersistentPropertyPath path; - - /** - * The parent {@link PathNode}. This is {@code null} if this is the root entity. - */ - @Nullable PathNode parent; - - /** The value of the entity. */ - Object value; - } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java new file mode 100644 index 000000000..b78481727 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017-2018 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 + * + * http://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.conversion; + +import lombok.RequiredArgsConstructor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.conversion.AggregateChange.Kind; +import org.springframework.data.relational.core.conversion.DbAction.InsertRoot; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; + +import static org.assertj.core.api.Assertions.*; + +/** + * Unit tests for the {@link RelationalEntityInsertWriter} + * + * @author Jens Schauder + * @author Thomas Lang + */ +@RunWith(MockitoJUnitRunner.class) public class RelationalEntityInsertWriterUnitTests { + + public static final long SOME_ENTITY_ID = 23L; + RelationalEntityInsertWriter converter = new RelationalEntityInsertWriter(new RelationalMappingContext()); + + @Test // DATAJDBC-112 + public void newEntityGetsConvertedToOneInsert() { + + SingleReferenceEntity entity = new SingleReferenceEntity(null); + AggregateChange aggregateChange = // + new AggregateChange(Kind.SAVE, SingleReferenceEntity.class, entity); + + converter.write(entity, aggregateChange); + + assertThat(aggregateChange.getActions()) // + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // + .containsExactly( // + tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) // + ); + } + + @Test // DATAJDBC-282 + public void existingEntityGetsNotConvertedToDeletePlusUpdate() { + + SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); + + AggregateChange aggregateChange = // + new AggregateChange(Kind.SAVE, SingleReferenceEntity.class, entity); + + converter.write(entity, aggregateChange); + + assertThat(aggregateChange.getActions()) // + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // + .containsExactly( // + tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) // + ); + + assertThat(aggregateChange.getEntity()).isNotNull(); + // the new id should not be the same as the origin one - should do insert, not update + // assertThat(aggregateChange.getEntity().id).isNotEqualTo(SOME_ENTITY_ID); + } + + private String extractPath(DbAction action) { + + if (action instanceof DbAction.WithPropertyPath) { + return ((DbAction.WithPropertyPath) action).getPropertyPath().toDotPath(); + } + + return ""; + } + + private boolean isWithDependsOn(DbAction dbAction) { + return dbAction instanceof DbAction.WithDependingOn; + } + + private Class actualEntityType(DbAction a) { + + if (a instanceof DbAction.WithEntity) { + return ((DbAction.WithEntity) a).getEntity().getClass(); + } + return null; + } + + @RequiredArgsConstructor static class SingleReferenceEntity { + + @Id final Long id; + Element other; + // should not trigger own Dbaction + String name; + } + + @RequiredArgsConstructor private static class Element { + @Id final Long id; + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java new file mode 100644 index 000000000..6d9a711cb --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2017-2018 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 + * + * http://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.conversion; + +import lombok.RequiredArgsConstructor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.conversion.AggregateChange.Kind; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; + +import static org.assertj.core.api.Assertions.*; + +/** + * Unit tests for the {@link RelationalEntityUpdateWriter} + * + * @author Jens Schauder + * @author Thomas Lang + */ +@RunWith(MockitoJUnitRunner.class) +public class RelationalEntityUpdateWriterUnitTests { + + public static final long SOME_ENTITY_ID = 23L; + RelationalEntityUpdateWriter converter = new RelationalEntityUpdateWriter(new RelationalMappingContext()); + + @Test // DATAJDBC-112 + public void existingEntityGetsConvertedToDeletePlusUpdate() { + + SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); + + AggregateChange aggregateChange = // + new AggregateChange(Kind.SAVE, SingleReferenceEntity.class, entity); + + converter.write(entity, aggregateChange); + + assertThat(aggregateChange.getActions()) // + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // + .containsExactly( // + tuple(DbAction.Delete.class, Element.class, "other", null, false), // + tuple(DbAction.UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) // + ); + } + + private String extractPath(DbAction action) { + + if (action instanceof DbAction.WithPropertyPath) { + return ((DbAction.WithPropertyPath) action).getPropertyPath().toDotPath(); + } + + return ""; + } + + private boolean isWithDependsOn(DbAction dbAction) { + return dbAction instanceof DbAction.WithDependingOn; + } + + private Class actualEntityType(DbAction a) { + + if (a instanceof DbAction.WithEntity) { + return ((DbAction.WithEntity) a).getEntity().getClass(); + } + return null; + } + + @RequiredArgsConstructor + static class SingleReferenceEntity { + + @Id + final Long id; + Element other; + // should not trigger own Dbaction + String name; + } + + @RequiredArgsConstructor + private static class Element { + @Id + final Long id; + } + +}