Browse Source

DATAJDBC-282 - Add insert and update methods to JdbcRepository.

This introduces the JdbcRepository interface which offers two additional methods beyond the CrudRepository:
`insert` and `update`.
Both methods skip the test if the aggregate is new and perform the respective operation.

Especially `insert` is useful for saving new aggregates which are new but have an ID set by the client and not generated by the database.

Original pull request: #107.
pull/108/head
Thomas Lang 7 years ago committed by Jens Schauder
parent
commit
d049988028
  1. 164
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java
  2. 122
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java
  3. 18
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcRepository.java
  4. 227
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java
  5. 324
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java
  6. 274
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AbstractRelationalEntityWriter.java
  7. 44
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriter.java
  8. 44
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriter.java
  9. 234
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java
  10. 111
      spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java
  11. 96
      spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java

164
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 { public interface JdbcAggregateOperations {
/** /**
* Saves an instance of an aggregate, including all the members of the aggregate. * 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 instance the aggregate root of the aggregate to be saved. Must not be {@code null}.
* @param <T> the type of the aggregate root. * @param <T> the type of the aggregate root.
* @return the saved instance. * @return the saved instance.
*/ */
<T> T save(T instance); <T> T save(T instance);
/** /**
* Deletes a single Aggregate including all entities contained in that aggregate. * Dedicated insert function to do just the insert of an instance of an aggregate, including all the members of the aggregate.
* *
* @param id the id of the aggregate root of the aggregate to be deleted. Must not be {@code null}. * @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}.
* @param domainType the type of the aggregate root. * @param <T> the type of the aggregate root.
* @param <T> the type of the aggregate root. * @return the saved instance.
*/ */
<T> void deleteById(Object id, Class<T> domainType); <T> T insert(T instance);
/** /**
* Delete an aggregate identified by it's aggregate root. * Dedicated update function to do just an update of an instance of an aggregate, including all the members of the aggregate.
* *
* @param aggregateRoot to delete. Must not be {@code null}. * @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}.
* @param domainType the type of the aggregate root. Must not be {@code null}. * @param <T> the type of the aggregate root.
* @param <T> the type of the aggregate root. * @return the saved instance.
*/ */
<T> void delete(T aggregateRoot, Class<T> domainType); <T> T update(T instance);
/** /**
* Delete all aggregates of a given type. * Deletes a single Aggregate including all entities contained in that aggregate.
* *
* @param domainType type of the aggregate roots to be deleted. Must not be {@code null}. * @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.
void deleteAll(Class<?> domainType); * @param <T> the type of the aggregate root.
*/
<T> void deleteById(Object id, Class<T> domainType);
/** /**
* Counts the number of aggregates of a given type. * Delete an aggregate identified by it's aggregate root.
* *
* @param domainType the type of the aggregates to be counted. * @param aggregateRoot to delete. Must not be {@code null}.
* @return the number of instances stored in the database. Guaranteed to be not {@code null}. * @param domainType the type of the aggregate root. Must not be {@code null}.
*/ * @param <T> the type of the aggregate root.
long count(Class<?> domainType); */
<T> void delete(T aggregateRoot, Class<T> domainType);
/** /**
* Load an aggregate from the database. * Delete all aggregates of a given type.
* *
* @param id the id of the aggregate to load. Must not be {@code null}. * @param domainType type of the aggregate roots to be deleted. Must not be {@code null}.
* @param domainType the type of the aggregate root. Must not be {@code null}. */
* @param <T> the type of the aggregate root. void deleteAll(Class<?> domainType);
* @return the loaded aggregate. Might return {@code null}.
*/
@Nullable
<T> T findById(Object id, Class<T> domainType);
/** /**
* Load all aggregates of a given type that are identified by the given ids. * Counts the number of aggregates of a given type.
* *
* @param ids of the aggregate roots identifying the aggregates to load. Must not be {@code null}. * @param domainType the type of the aggregates to be counted.
* @param domainType the type of the aggregate roots. Must not be {@code null}. * @return the number of instances stored in the database. Guaranteed to be not {@code null}.
* @param <T> the type of the aggregate roots. Must not be {@code null}. */
* @return Guaranteed to be not {@code null}. long count(Class<?> domainType);
*/
<T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType);
/** /**
* Load all aggregates of a given type. * Load an aggregate from the database.
* *
* @param domainType the type of the aggregate roots. Must not be {@code null}. * @param id the id of the aggregate to load. Must not be {@code null}.
* @param <T> the type of the aggregate roots. Must not be {@code null}. * @param domainType the type of the aggregate root. Must not be {@code null}.
* @return Guaranteed to be not {@code null}. * @param <T> the type of the aggregate root.
*/ * @return the loaded aggregate. Might return {@code null}.
<T> Iterable<T> findAll(Class<T> domainType); */
@Nullable
<T> T findById(Object id, Class<T> domainType);
/** /**
* Checks if an aggregate identified by type and id exists in the database. * Load all aggregates of a given type that are identified by the given ids.
* *
* @param id the id of the aggregate root. * @param ids of the aggregate roots identifying the aggregates to load. Must not be {@code null}.
* @param domainType the type of the aggregate root. * @param domainType the type of the aggregate roots. Must not be {@code null}.
* @param <T> the type of the aggregate root. * @param <T> the type of the aggregate roots. Must not be {@code null}.
* @return whether the aggregate exists. * @return Guaranteed to be not {@code null}.
*/ */
<T> boolean existsById(Object id, Class<T> domainType); <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType);
/**
* Load all aggregates of a given type.
*
* @param domainType the type of the aggregate roots. Must not be {@code null}.
* @param <T> the type of the aggregate roots. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Iterable<T> findAll(Class<T> 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 <T> the type of the aggregate root.
* @return whether the aggregate exists.
*/
<T> boolean existsById(Object id, Class<T> domainType);
} }

122
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.Interpreter;
import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter; 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.conversion.RelationalEntityWriter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@ -52,15 +54,41 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
private final RelationalEntityWriter jdbcEntityWriter; private final RelationalEntityWriter jdbcEntityWriter;
private final RelationalEntityDeleteWriter jdbcEntityDeleteWriter; private final RelationalEntityDeleteWriter jdbcEntityDeleteWriter;
private final RelationalEntityInsertWriter jdbcEntityInsertWriter;
private final RelationalEntityUpdateWriter jdbcEntityUpdateWriter;
private final DataAccessStrategy accessStrategy; private final DataAccessStrategy accessStrategy;
private <T> T store(T instance, IdentifierAccessor identifierAccessor, AggregateChange<T> 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}, * Creates a new {@link JdbcAggregateTemplate} given {@link ApplicationEventPublisher},
* {@link RelationalMappingContext} and {@link DataAccessStrategy}. * {@link RelationalMappingContext} and {@link DataAccessStrategy}.
* *
* @param publisher must not be {@literal null}. * @param publisher must not be {@literal null}.
* @param context must not be {@literal null}. * @param context must not be {@literal null}.
* @param dataAccessStrategy must not be {@literal null}. * @param dataAccessStrategy must not be {@literal null}.
*/ */
public JdbcAggregateTemplate(ApplicationEventPublisher publisher, RelationalMappingContext context, public JdbcAggregateTemplate(ApplicationEventPublisher publisher, RelationalMappingContext context,
@ -77,6 +105,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
this.accessStrategy = dataAccessStrategy; this.accessStrategy = dataAccessStrategy;
this.jdbcEntityWriter = new RelationalEntityWriter(context); this.jdbcEntityWriter = new RelationalEntityWriter(context);
this.jdbcEntityInsertWriter = new RelationalEntityInsertWriter(context);
this.jdbcEntityUpdateWriter = new RelationalEntityUpdateWriter(context);
this.jdbcEntityDeleteWriter = new RelationalEntityDeleteWriter(context); this.jdbcEntityDeleteWriter = new RelationalEntityDeleteWriter(context);
this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy); this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy);
} }
@ -85,8 +115,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#save(java.lang.Object) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#save(java.lang.Object)
*/ */
@Override @Override public <T> T save(T instance) {
public <T> T save(T instance) {
Assert.notNull(instance, "Aggregate instance must not be null!"); Assert.notNull(instance, "Aggregate instance must not be null!");
@ -95,33 +124,48 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
AggregateChange<T> change = createChange(instance); AggregateChange<T> change = createChange(instance);
publisher.publishEvent(new BeforeSaveEvent( // return store(instance, identifierAccessor, change, persistentEntity);
Identifier.ofNullable(identifierAccessor.getIdentifier()), // }
instance, //
change //
));
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> 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<T> change = createInsertChange(instance);
publisher.publishEvent(new AfterSaveEvent( // return store(instance, identifierAccessor, change, persistentEntity);
Identifier.of(identifier), // }
change.getEntity(), //
change //
));
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> 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<T> change = createUpdateChange(instance);
return store(instance, identifierAccessor, change, persistentEntity);
} }
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#count(java.lang.Class) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#count(java.lang.Class)
*/ */
@Override @Override public long count(Class<?> domainType) {
public long count(Class<?> domainType) {
return accessStrategy.count(domainType); return accessStrategy.count(domainType);
} }
@ -129,8 +173,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findById(java.lang.Object, java.lang.Class) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findById(java.lang.Object, java.lang.Class)
*/ */
@Override @Override public <T> T findById(Object id, Class<T> domainType) {
public <T> T findById(Object id, Class<T> domainType) {
T entity = accessStrategy.findById(id, domainType); T entity = accessStrategy.findById(id, domainType);
if (entity != null) { if (entity != null) {
@ -143,8 +186,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#existsById(java.lang.Object, java.lang.Class) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#existsById(java.lang.Object, java.lang.Class)
*/ */
@Override @Override public <T> boolean existsById(Object id, Class<T> domainType) {
public <T> boolean existsById(Object id, Class<T> domainType) {
return accessStrategy.existsById(id, domainType); return accessStrategy.existsById(id, domainType);
} }
@ -152,8 +194,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAll(java.lang.Class) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAll(java.lang.Class)
*/ */
@Override @Override public <T> Iterable<T> findAll(Class<T> domainType) {
public <T> Iterable<T> findAll(Class<T> domainType) {
Iterable<T> all = accessStrategy.findAll(domainType); Iterable<T> all = accessStrategy.findAll(domainType);
publishAfterLoad(all); publishAfterLoad(all);
@ -164,8 +205,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAllById(java.lang.Iterable, java.lang.Class) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAllById(java.lang.Iterable, java.lang.Class)
*/ */
@Override @Override public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
Iterable<T> allById = accessStrategy.findAllById(ids, domainType); Iterable<T> allById = accessStrategy.findAllById(ids, domainType);
publishAfterLoad(allById); publishAfterLoad(allById);
@ -176,8 +216,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#delete(java.lang.Object, java.lang.Class) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#delete(java.lang.Object, java.lang.Class)
*/ */
@Override @Override public <S> void delete(S aggregateRoot, Class<S> domainType) {
public <S> void delete(S aggregateRoot, Class<S> domainType) {
IdentifierAccessor identifierAccessor = context.getRequiredPersistentEntity(domainType) IdentifierAccessor identifierAccessor = context.getRequiredPersistentEntity(domainType)
.getIdentifierAccessor(aggregateRoot); .getIdentifierAccessor(aggregateRoot);
@ -189,8 +228,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#deleteById(java.lang.Object, java.lang.Class) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#deleteById(java.lang.Object, java.lang.Class)
*/ */
@Override @Override public <S> void deleteById(Object id, Class<S> domainType) {
public <S> void deleteById(Object id, Class<S> domainType) {
deleteTree(id, null, domainType); deleteTree(id, null, domainType);
} }
@ -198,8 +236,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#deleteAll(java.lang.Class) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#deleteAll(java.lang.Class)
*/ */
@Override @Override public void deleteAll(Class<?> domainType) {
public void deleteAll(Class<?> domainType) {
AggregateChange<?> change = createDeletingChange(domainType); AggregateChange<?> change = createDeletingChange(domainType);
change.executeWith(interpreter, context, converter); change.executeWith(interpreter, context, converter);
@ -218,14 +255,27 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
publisher.publishEvent(new AfterDeleteEvent(specifiedId, optionalEntity, change)); publisher.publishEvent(new AfterDeleteEvent(specifiedId, optionalEntity, change));
} }
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings({ "unchecked", "rawtypes" }) private <T> AggregateChange<T> createChange(T instance) {
private <T> AggregateChange<T> createChange(T instance) {
AggregateChange<T> aggregateChange = new AggregateChange(Kind.SAVE, instance.getClass(), instance); AggregateChange<T> aggregateChange = new AggregateChange(Kind.SAVE, instance.getClass(), instance);
jdbcEntityWriter.write(instance, aggregateChange); jdbcEntityWriter.write(instance, aggregateChange);
return aggregateChange; return aggregateChange;
} }
@SuppressWarnings({ "unchecked", "rawtypes" }) private <T> AggregateChange<T> createInsertChange(T instance) {
AggregateChange<T> aggregateChange = new AggregateChange(Kind.SAVE, instance.getClass(), instance);
jdbcEntityInsertWriter.write(instance, aggregateChange);
return aggregateChange;
}
@SuppressWarnings({ "unchecked", "rawtypes" }) private <T> AggregateChange<T> createUpdateChange(T instance) {
AggregateChange<T> aggregateChange = new AggregateChange(Kind.SAVE, instance.getClass(), instance);
jdbcEntityUpdateWriter.write(instance, aggregateChange);
return aggregateChange;
}
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings({ "unchecked", "rawtypes" })
private AggregateChange<?> createDeletingChange(Object id, @Nullable Object entity, Class<?> domainType) { private AggregateChange<?> createDeletingChange(Object id, @Nullable Object entity, Class<?> domainType) {

18
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 <a href="https://jira.spring.io/browse/DATAJDBC-282">DATAJDBC-282</a>
*/
public interface JdbcRepository<T, ID> extends Repository<T, ID> {
<S extends T> S insert(S var1);
<S extends T> S update(S var1);
}

227
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 java.util.stream.Collectors;
import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateOperations;
import org.springframework.data.jdbc.core.JdbcRepository;
import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.util.Streamable; import org.springframework.data.util.Streamable;
@ -31,107 +32,127 @@ import org.springframework.data.util.Streamable;
* @author Oliver Gierke * @author Oliver Gierke
*/ */
@RequiredArgsConstructor @RequiredArgsConstructor
public class SimpleJdbcRepository<T, ID> implements CrudRepository<T, ID> { public class SimpleJdbcRepository<T, ID> implements CrudRepository<T, ID>, JdbcRepository<T, ID> {
private final @NonNull JdbcAggregateOperations entityOperations; private final @NonNull
private final @NonNull PersistentEntity<T, ?> entity; JdbcAggregateOperations entityOperations;
private final @NonNull
/* PersistentEntity<T, ?> entity;
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#save(S) /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#save(S)
public <S extends T> S save(S instance) { */
return entityOperations.save(instance); @Override
} public <S extends T> S save(S instance) {
return entityOperations.save(instance);
/* }
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable)
public <S extends T> Iterable<S> saveAll(Iterable<S> entities) { */
@Override
return Streamable.of(entities).stream() // public <S extends T> Iterable<S> saveAll(Iterable<S> entities) {
.map(this::save) //
.collect(Collectors.toList()); return Streamable.of(entities).stream() //
} .map(this::save) //
.collect(Collectors.toList());
/* }
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable)
public Optional<T> findById(ID id) { */
return Optional.ofNullable(entityOperations.findById(id, entity.getType())); @Override
} public Optional<T> findById(ID id) {
return Optional.ofNullable(entityOperations.findById(id, entity.getType()));
/* }
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable) /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable)
public boolean existsById(ID id) { */
return entityOperations.existsById(id, entity.getType()); @Override
} public boolean existsById(ID id) {
return entityOperations.existsById(id, entity.getType());
/* }
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#findAll() /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#findAll()
public Iterable<T> findAll() { */
return entityOperations.findAll(entity.getType()); @Override
} public Iterable<T> findAll() {
return entityOperations.findAll(entity.getType());
/* }
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable)
public Iterable<T> findAllById(Iterable<ID> ids) { */
return entityOperations.findAllById(ids, entity.getType()); @Override
} public Iterable<T> findAllById(Iterable<ID> ids) {
return entityOperations.findAllById(ids, entity.getType());
/* }
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#count() /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#count()
public long count() { */
return entityOperations.count(entity.getType()); @Override
} public long count() {
return entityOperations.count(entity.getType());
/* }
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable)
public void deleteById(ID id) { */
entityOperations.deleteById(id, entity.getType()); @Override
} public void deleteById(ID id) {
entityOperations.deleteById(id, entity.getType());
/* }
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object)
public void delete(T instance) { */
entityOperations.delete(instance, entity.getType()); @Override
} public void delete(T instance) {
entityOperations.delete(instance, entity.getType());
/* }
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable) /*
*/ * (non-Javadoc)
@Override * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable)
@SuppressWarnings("unchecked") */
public void deleteAll(Iterable<? extends T> entities) { @Override
entities.forEach(it -> entityOperations.delete(it, (Class<T>) it.getClass())); @SuppressWarnings("unchecked")
} public void deleteAll(Iterable<? extends T> entities) {
entities.forEach(it -> entityOperations.delete(it, (Class<T>) it.getClass()));
@Override }
public void deleteAll() {
entityOperations.deleteAll(entity.getType()); @Override
} public void deleteAll() {
entityOperations.deleteAll(entity.getType());
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.JdbcRepository#insert(T t)
*/
@Override
public <S extends T> S insert(S var1) {
return entityOperations.insert(var1);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.JdbcRepository#update(T t)
*/
@Override
public <S extends T> S update(S var1) {
return entityOperations.update(var1);
}
} }

324
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.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.data.annotation.Id; 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.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
@ -48,213 +49,246 @@ import org.springframework.transaction.annotation.Transactional;
@Transactional @Transactional
public class JdbcRepositoryIntegrationTests { public class JdbcRepositoryIntegrationTests {
@Configuration @Configuration
@Import(TestConfiguration.class) @Import(TestConfiguration.class)
static class Config { static class Config {
@Autowired JdbcRepositoryFactory factory; @Autowired
JdbcRepositoryFactory factory;
@Bean @Bean
Class<?> testClass() { Class<?> testClass() {
return JdbcRepositoryIntegrationTests.class; return JdbcRepositoryIntegrationTests.class;
} }
@Bean @Bean
DummyEntityRepository dummyEntityRepository() { DummyEntityRepository dummyEntityRepository() {
return factory.getRepository(DummyEntityRepository.class); return factory.getRepository(DummyEntityRepository.class);
} }
} }
@ClassRule public static final SpringClassRule classRule = new SpringClassRule(); @ClassRule
@Rule public SpringMethodRule methodRule = new SpringMethodRule(); public static final SpringClassRule classRule = new SpringClassRule();
@Rule
public SpringMethodRule methodRule = new SpringMethodRule();
@Autowired NamedParameterJdbcTemplate template; @Autowired
@Autowired DummyEntityRepository repository; NamedParameterJdbcTemplate template;
@Autowired
DummyEntityRepository repository;
@Test // DATAJDBC-95 @Test // DATAJDBC-95
public void savesAnEntity() { public void savesAnEntity() {
DummyEntity entity = repository.save(createDummyEntity()); DummyEntity entity = repository.save(createDummyEntity());
assertThat(JdbcTestUtils.countRowsInTableWhere((JdbcTemplate) template.getJdbcOperations(), "dummy_entity", assertThat(JdbcTestUtils.countRowsInTableWhere((JdbcTemplate) template.getJdbcOperations(), "dummy_entity",
"id_Prop = " + entity.getIdProp())).isEqualTo(1); "id_Prop = " + entity.getIdProp())).isEqualTo(1);
} }
@Test // DATAJDBC-95 @Test // DATAJDBC-282
public void saveAndLoadAnEntity() { 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()); @Test // DATAJDBC-282
assertThat(it.getName()).isEqualTo(entity.getName()); public void insertAnExistingEntity() {
});
}
@Test // DATAJDBC-97 DummyEntity existingDummyEntity = createExistingDummyEntity();
public void savesManyEntities() { DummyEntity entity = repository.insert(existingDummyEntity);
assertThat(JdbcTestUtils.countRowsInTableWhere((JdbcTemplate) template.getJdbcOperations(), "dummy_entity",
"id_Prop = " + existingDummyEntity.getIdProp())).isEqualTo(1);
}
DummyEntity entity = createDummyEntity(); @Test // DATAJDBC-95
DummyEntity other = createDummyEntity(); public void saveAndLoadAnEntity() {
repository.saveAll(asList(entity, other)); DummyEntity entity = repository.save(createDummyEntity());
assertThat(repository.findAll()) // assertThat(repository.findById(entity.getIdProp())).hasValueSatisfying(it -> {
.extracting(DummyEntity::getIdProp) //
.containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp());
}
@Test // DATAJDBC-97 assertThat(it.getIdProp()).isEqualTo(entity.getIdProp());
public void existsReturnsTrueIffEntityExists() { assertThat(it.getName()).isEqualTo(entity.getName());
});
}
DummyEntity entity = repository.save(createDummyEntity()); @Test // DATAJDBC-97
public void savesManyEntities() {
assertThat(repository.existsById(entity.getIdProp())).isTrue(); DummyEntity entity = createDummyEntity();
assertThat(repository.existsById(entity.getIdProp() + 1)).isFalse(); DummyEntity other = createDummyEntity();
}
@Test // DATAJDBC-97 repository.saveAll(asList(entity, other));
public void findAllFindsAllEntities() {
DummyEntity entity = repository.save(createDummyEntity()); assertThat(repository.findAll()) //
DummyEntity other = repository.save(createDummyEntity()); .extracting(DummyEntity::getIdProp) //
.containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp());
}
Iterable<DummyEntity> all = repository.findAll(); @Test // DATAJDBC-97
public void existsReturnsTrueIffEntityExists() {
assertThat(all)// DummyEntity entity = repository.save(createDummyEntity());
.extracting(DummyEntity::getIdProp)//
.containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp());
}
@Test // DATAJDBC-97 assertThat(repository.existsById(entity.getIdProp())).isTrue();
public void findAllFindsAllSpecifiedEntities() { assertThat(repository.existsById(entity.getIdProp() + 1)).isFalse();
}
DummyEntity entity = repository.save(createDummyEntity()); @Test // DATAJDBC-97
DummyEntity other = repository.save(createDummyEntity()); public void findAllFindsAllEntities() {
assertThat(repository.findAllById(asList(entity.getIdProp(), other.getIdProp())))// DummyEntity entity = repository.save(createDummyEntity());
.extracting(DummyEntity::getIdProp)// DummyEntity other = repository.save(createDummyEntity());
.containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp());
}
@Test // DATAJDBC-97 Iterable<DummyEntity> all = repository.findAll();
public void countsEntities() {
repository.save(createDummyEntity()); assertThat(all)//
repository.save(createDummyEntity()); .extracting(DummyEntity::getIdProp)//
repository.save(createDummyEntity()); .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp());
}
assertThat(repository.count()).isEqualTo(3L); @Test // DATAJDBC-97
} public void findAllFindsAllSpecifiedEntities() {
@Test // DATAJDBC-97 DummyEntity entity = repository.save(createDummyEntity());
public void deleteById() { DummyEntity other = repository.save(createDummyEntity());
DummyEntity one = repository.save(createDummyEntity()); assertThat(repository.findAllById(asList(entity.getIdProp(), other.getIdProp())))//
DummyEntity two = repository.save(createDummyEntity()); .extracting(DummyEntity::getIdProp)//
DummyEntity three = repository.save(createDummyEntity()); .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp());
}
repository.deleteById(two.getIdProp()); @Test // DATAJDBC-97
public void countsEntities() {
assertThat(repository.findAll()) // repository.save(createDummyEntity());
.extracting(DummyEntity::getIdProp) // repository.save(createDummyEntity());
.containsExactlyInAnyOrder(one.getIdProp(), three.getIdProp()); repository.save(createDummyEntity());
}
@Test // DATAJDBC-97 assertThat(repository.count()).isEqualTo(3L);
public void deleteByEntity() { }
DummyEntity one = repository.save(createDummyEntity()); @Test // DATAJDBC-97
DummyEntity two = repository.save(createDummyEntity()); public void deleteById() {
DummyEntity three = repository.save(createDummyEntity());
repository.delete(one); DummyEntity one = repository.save(createDummyEntity());
DummyEntity two = repository.save(createDummyEntity());
DummyEntity three = repository.save(createDummyEntity());
assertThat(repository.findAll()) // repository.deleteById(two.getIdProp());
.extracting(DummyEntity::getIdProp) //
.containsExactlyInAnyOrder(two.getIdProp(), three.getIdProp());
}
@Test // DATAJDBC-97 assertThat(repository.findAll()) //
public void deleteByList() { .extracting(DummyEntity::getIdProp) //
.containsExactlyInAnyOrder(one.getIdProp(), three.getIdProp());
}
DummyEntity one = repository.save(createDummyEntity()); @Test // DATAJDBC-97
DummyEntity two = repository.save(createDummyEntity()); public void deleteByEntity() {
DummyEntity three = repository.save(createDummyEntity());
repository.deleteAll(asList(one, three)); DummyEntity one = repository.save(createDummyEntity());
DummyEntity two = repository.save(createDummyEntity());
DummyEntity three = repository.save(createDummyEntity());
assertThat(repository.findAll()) // repository.delete(one);
.extracting(DummyEntity::getIdProp) //
.containsExactlyInAnyOrder(two.getIdProp());
}
@Test // DATAJDBC-97 assertThat(repository.findAll()) //
public void deleteAll() { .extracting(DummyEntity::getIdProp) //
.containsExactlyInAnyOrder(two.getIdProp(), three.getIdProp());
}
repository.save(createDummyEntity()); @Test // DATAJDBC-97
repository.save(createDummyEntity()); public void deleteByList() {
repository.save(createDummyEntity());
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 @Test // DATAJDBC-97
public void update() { public void deleteAll() {
DummyEntity entity = repository.save(createDummyEntity()); repository.save(createDummyEntity());
repository.save(createDummyEntity());
repository.save(createDummyEntity());
entity.setName("something else"); assertThat(repository.findAll()).isNotEmpty();
DummyEntity saved = repository.save(entity);
assertThat(repository.findById(entity.getIdProp())).hasValueSatisfying(it -> { repository.deleteAll();
assertThat(it.getName()).isEqualTo(saved.getName());
});
}
@Test // DATAJDBC-98 assertThat(repository.findAll()).isEmpty();
public void updateMany() { }
DummyEntity entity = repository.save(createDummyEntity()); @Test // DATAJDBC-98
DummyEntity other = repository.save(createDummyEntity()); public void update() {
entity.setName("something else"); DummyEntity entity = repository.save(createDummyEntity());
other.setName("others Name");
repository.saveAll(asList(entity, other)); entity.setName("something else");
DummyEntity saved = repository.save(entity);
assertThat(repository.findAll()) // assertThat(repository.findById(entity.getIdProp())).hasValueSatisfying(it -> {
.extracting(DummyEntity::getName) // assertThat(it.getName()).isEqualTo(saved.getName());
.containsExactlyInAnyOrder(entity.getName(), other.getName()); });
} }
@Test // DATAJDBC-112 @Test // DATAJDBC-98
public void findByIdReturnsEmptyWhenNoneFound() { 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(); assertThat(repository.findAll()) //
entity.setName("Entity Name"); .extracting(DummyEntity::getName) //
return entity; .containsExactlyInAnyOrder(entity.getName(), other.getName());
} }
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {} @Test // DATAJDBC-112
public void findByIdReturnsEmptyWhenNoneFound() {
@Data // NOT saving anything, so DB is empty
static class DummyEntity {
String name; assertThat(repository.findById(-1L)).isEmpty();
@Id private Long idProp; }
}
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<DummyEntity, Long>, JdbcRepository<DummyEntity, Long> {
}
@Data
static class DummyEntity {
String name;
@Id
private Long idProp;
}
} }

274
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<Object, AggregateChange<?>> {
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<?, RelationalPersistentProperty> paths;
private final Map<PathNode, DbAction> previousActions = new HashMap<>();
private Map<PersistentPropertyPath<RelationalPersistentProperty>, List<PathNode>> 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 <a href="https://jira.spring.io/browse/DATAJDBC-282">DAJDBC-282</a>
*/
List<DbAction<?>> insert() {
List<DbAction<?>> 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 <a href="https://jira.spring.io/browse/DATAJDBC-282">DAJDBC-282</a>
*/
List<DbAction<?>> update() {
List<DbAction<?>> actions = new ArrayList<>(deleteReferenced());
actions.add(setRootAction(new DbAction.UpdateRoot<>(entity)));
actions.addAll(insertReferenced());
return actions;
}
List<DbAction<?>> save() {
List<DbAction<?>> 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<DbAction<?>> insertReferenced() {
List<DbAction<?>> actions = new ArrayList<>();
paths.forEach(path -> actions.addAll(insertAll(path)));
return actions;
}
private List<DbAction<?>> insertAll(PersistentPropertyPath<RelationalPersistentProperty> path) {
List<DbAction<?>> actions = new ArrayList<>();
from(path).forEach(node -> {
DbAction.Insert<Object> insert;
if (node.path.getRequiredLeafProperty().isQualified()) {
Pair<Object, Object> 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<DbAction<?>> deleteReferenced() {
List<DbAction<?>> deletes = new ArrayList<>();
paths.forEach(path -> deletes.add(0, deleteReferenced(path)));
return deletes;
}
/// Operations on a single path
private DbAction.Delete<?> deleteReferenced(PersistentPropertyPath<RelationalPersistentProperty> 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<RelationalEntityInsertWriter.PathNode> from(
PersistentPropertyPath<RelationalPersistentProperty> path) {
List<RelationalEntityInsertWriter.PathNode> 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<RelationalEntityInsertWriter.PathNode> 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<RelationalEntityInsertWriter.PathNode> createNodes(
PersistentPropertyPath<RelationalPersistentProperty> path,
@Nullable RelationalEntityInsertWriter.PathNode parentNode, @Nullable Object value) {
if (value == null) {
return Collections.emptyList();
}
List<RelationalEntityInsertWriter.PathNode> 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<RelationalPersistentProperty> 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;
}
}

44
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<DbAction<?>> actions = new WritingContext(root, aggregateChange).insert();
actions.forEach(aggregateChange::addAction);
}
}

44
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<DbAction<?>> actions = new WritingContext(root, aggregateChange).update();
actions.forEach(aggregateChange::addAction);
}
}

234
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; 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.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}. * Converts an aggregate represented by its root into an {@link AggregateChange}.
@ -40,223 +25,18 @@ import org.springframework.util.Assert;
* @author Jens Schauder * @author Jens Schauder
* @author Mark Paluch * @author Mark Paluch
*/ */
public class RelationalEntityWriter implements EntityWriter<Object, AggregateChange<?>> { public class RelationalEntityWriter extends AbstractRelationalEntityWriter {
private final RelationalMappingContext context;
public RelationalEntityWriter(RelationalMappingContext context) { public RelationalEntityWriter(RelationalMappingContext context) {
this.context = context; super(context);
} }
/* /*
* (non-Javadoc) * (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 @Override public void write(Object root, AggregateChange<?> aggregateChange) {
public void write(Object root, AggregateChange<?> aggregateChange) { List<DbAction<?>> actions = new WritingContext(root, aggregateChange).save();
List<DbAction<?>> actions = new WritingContext(root, aggregateChange).write();
actions.forEach(aggregateChange::addAction); 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<?, RelationalPersistentProperty> paths;
private final Map<PathNode, DbAction> previousActions = new HashMap<>();
private Map<PersistentPropertyPath<RelationalPersistentProperty>, List<PathNode>> 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<DbAction<?>> write() {
List<DbAction<?>> 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<DbAction<?>> insertReferenced() {
List<DbAction<?>> actions = new ArrayList<>();
paths.forEach(path -> actions.addAll(insertAll(path)));
return actions;
}
private List<DbAction<?>> insertAll(PersistentPropertyPath<RelationalPersistentProperty> path) {
List<DbAction<?>> actions = new ArrayList<>();
from(path).forEach(node -> {
DbAction.Insert<Object> insert;
if (node.path.getRequiredLeafProperty().isQualified()) {
Pair<Object, Object> 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<DbAction<?>> deleteReferenced() {
List<DbAction<?>> deletes = new ArrayList<>();
paths.forEach(path -> deletes.add(0, deleteReferenced(path)));
return deletes;
}
/// Operations on a single path
private DbAction.Delete<?> deleteReferenced(PersistentPropertyPath<RelationalPersistentProperty> 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<PathNode> from(PersistentPropertyPath<RelationalPersistentProperty> path) {
List<PathNode> 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<PathNode> 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<PathNode> createNodes(PersistentPropertyPath<RelationalPersistentProperty> path,
@Nullable PathNode parentNode, @Nullable Object value) {
if (value == null) {
return Collections.emptyList();
}
List<PathNode> 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<RelationalPersistentProperty> 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;
}
} }

111
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<SingleReferenceEntity> 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<SingleReferenceEntity> 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;
}
}

96
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<RelationalEntityWriterUnitTests.SingleReferenceEntity> 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;
}
}
Loading…
Cancel
Save