DATAJDBC-227 - Refactored JdbcEntityWriter and DbActions.

JdbcEntityWriter and JdbcDeleteEntityWriter now use an iterative approach based on PersistentPropertyPath instead of a recursive one.

DbAction is now split into multiple interfaces representing different variants of actions.
The implementations are simple value types without any implementation inheritance hierarchy.
All elements of a DbAction implementation are not null making usage and construction of instances much easier.

Original pull request: #79.
This commit is contained in:
Jens Schauder
2018-06-12 14:24:20 +02:00
committed by Mark Paluch
parent c412aad6b8
commit bdefd3da9a
30 changed files with 926 additions and 808 deletions

View File

@@ -21,7 +21,7 @@ import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
/**
@@ -45,8 +45,8 @@ public class CascadingDataAccessStrategy implements DataAccessStrategy {
}
@Override
public <S> void update(S instance, Class<S> domainType) {
collectVoid(das -> das.update(instance, domainType));
public <S> boolean update(S instance, Class<S> domainType) {
return collect(das -> das.update(instance, domainType));
}
@Override
@@ -55,7 +55,7 @@ public class CascadingDataAccessStrategy implements DataAccessStrategy {
}
@Override
public void delete(Object rootId, PropertyPath propertyPath) {
public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
collectVoid(das -> das.delete(rootId, propertyPath));
}
@@ -65,7 +65,7 @@ public class CascadingDataAccessStrategy implements DataAccessStrategy {
}
@Override
public void deleteAll(PropertyPath propertyPath) {
public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
collectVoid(das -> das.deleteAll(propertyPath));
}

View File

@@ -17,7 +17,7 @@ package org.springframework.data.jdbc.core;
import java.util.Map;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.lang.Nullable;
@@ -48,8 +48,9 @@ public interface DataAccessStrategy {
* @param instance the instance to save. Must not be {@code null}.
* @param domainType the type of the instance to save. Must not be {@code null}.
* @param <T> the type of the instance to save.
* @return wether the update actually updated a row.
*/
<T> void update(T instance, Class<T> domainType);
<T> boolean update(T instance, Class<T> domainType);
/**
* deletes a single row identified by the id, from the table identified by the domainType. Does not handle cascading
@@ -63,11 +64,11 @@ public interface DataAccessStrategy {
/**
* Deletes all entities reachable via {@literal propertyPath} from the instance identified by {@literal rootId}.
*
*
* @param rootId Id of the root object on which the {@literal propertyPath} is based. Must not be {@code null}.
* @param propertyPath Leading from the root object to the entities to be deleted. Must not be {@code null}.
*/
void delete(Object rootId, PropertyPath propertyPath);
void delete(Object rootId, PersistentPropertyPath<RelationalPersistentProperty> propertyPath);
/**
* Deletes all entities of the given domain type.
@@ -82,7 +83,7 @@ public interface DataAccessStrategy {
*
* @param propertyPath Leading from the root object to the entities to be deleted. Must not be {@code null}.
*/
void deleteAll(PropertyPath propertyPath);
void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath);
/**
* Counts the rows in the table representing the given domain type.

View File

@@ -29,8 +29,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@@ -120,11 +120,11 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
* @see org.springframework.data.jdbc.core.DataAccessStrategy#update(java.lang.Object, java.lang.Class)
*/
@Override
public <S> void update(S instance, Class<S> domainType) {
public <S> boolean update(S instance, Class<S> domainType) {
RelationalPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
operations.update(sql(domainType).getUpdate(), getPropertyMap(instance, persistentEntity));
return operations.update(sql(domainType).getUpdate(), getPropertyMap(instance, persistentEntity)) != 0;
}
/*
@@ -145,11 +145,12 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PropertyPath)
*/
@Override
public void delete(Object rootId, PropertyPath propertyPath) {
public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
RelationalPersistentEntity<?> rootEntity = context.getRequiredPersistentEntity(propertyPath.getOwningType());
RelationalPersistentEntity<?> rootEntity = context
.getRequiredPersistentEntity(propertyPath.getBaseProperty().getOwner().getType());
RelationalPersistentProperty referencingProperty = rootEntity.getRequiredPersistentProperty(propertyPath.getSegment());
RelationalPersistentProperty referencingProperty = propertyPath.getLeafProperty();
Assert.notNull(referencingProperty, "No property found matching the PropertyPath " + propertyPath);
String format = sql(rootEntity.getType()).createDeleteByPath(propertyPath);
@@ -173,8 +174,9 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
* @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteAll(org.springframework.data.mapping.PropertyPath)
*/
@Override
public void deleteAll(PropertyPath propertyPath) {
operations.getJdbcOperations().update(sql(propertyPath.getOwningType().getType()).createDeleteAllSql(propertyPath));
public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
operations.getJdbcOperations()
.update(sql(propertyPath.getBaseProperty().getOwner().getType()).createDeleteAllSql(propertyPath));
}
/*
@@ -343,7 +345,10 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
} catch (InvalidDataAccessApiUsageException e) {
// Postgres returns a value for each column
Map<String, Object> keys = holder.getKeys();
return Optional.ofNullable(keys == null ? null : keys.get(persistentEntity.getIdColumn()));
return Optional.ofNullable( //
keys == null || persistentEntity.getIdProperty() == null //
? null //
: keys.get(persistentEntity.getIdColumn()));
}
}

View File

@@ -18,17 +18,22 @@ package org.springframework.data.jdbc.core;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.conversion.Interpreter;
import org.springframework.data.relational.core.conversion.DbAction.Delete;
import org.springframework.data.relational.core.conversion.DbAction.DeleteAll;
import org.springframework.data.relational.core.conversion.DbAction.DeleteAllRoot;
import org.springframework.data.relational.core.conversion.DbAction.DeleteRoot;
import org.springframework.data.relational.core.conversion.DbAction.Insert;
import org.springframework.data.relational.core.conversion.DbAction.InsertRoot;
import org.springframework.data.relational.core.conversion.DbAction.Merge;
import org.springframework.data.relational.core.conversion.DbAction.Update;
import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot;
import org.springframework.data.relational.core.conversion.Interpreter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link Interpreter} for {@link DbAction}s using a {@link DataAccessStrategy} for performing actual database
@@ -53,51 +58,66 @@ class DefaultJdbcInterpreter implements Interpreter {
accessStrategy.insert(insert.getEntity(), insert.getEntityType(), createAdditionalColumnValues(insert));
}
@Override
public <T> void interpret(InsertRoot<T> insert) {
accessStrategy.insert(insert.getEntity(), insert.getEntityType(), new HashMap<>());
}
@Override
public <T> void interpret(Update<T> update) {
accessStrategy.update(update.getEntity(), update.getEntityType());
}
@Override
public <T> void interpret(Delete<T> delete) {
public <T> void interpret(UpdateRoot<T> update) {
accessStrategy.update(update.getEntity(), update.getEntityType());
}
if (delete.getPropertyPath() == null) {
accessStrategy.delete(delete.getRootId(), delete.getEntityType());
} else {
accessStrategy.delete(delete.getRootId(), delete.getPropertyPath().getPath());
@Override
public <T> void interpret(Merge<T> merge) {
// temporary implementation
if (!accessStrategy.update(merge.getEntity(), merge.getEntityType())) {
accessStrategy.insert(merge.getEntity(), merge.getEntityType(), createAdditionalColumnValues(merge));
}
}
@Override
public <T> void interpret(DeleteAll<T> delete) {
if (delete.getEntityType() == null) {
accessStrategy.deleteAll(delete.getPropertyPath().getPath());
} else {
accessStrategy.deleteAll(delete.getEntityType());
}
public <T> void interpret(Delete<T> delete) {
accessStrategy.delete(delete.getRootId(), delete.getPropertyPath());
}
private <T> Map<String, Object> createAdditionalColumnValues(Insert<T> insert) {
@Override
public <T> void interpret(DeleteRoot<T> delete) {
accessStrategy.delete(delete.getRootId(), delete.getEntityType());
}
@Override
public <T> void interpret(DeleteAll<T> delete) {
accessStrategy.deleteAll(delete.getPropertyPath());
}
@Override
public <T> void interpret(DeleteAllRoot<T> deleteAllRoot) {
accessStrategy.deleteAll(deleteAllRoot.getEntityType());
}
private <T> Map<String, Object> createAdditionalColumnValues(DbAction.WithDependingOn<T> action) {
Map<String, Object> additionalColumnValues = new HashMap<>();
addDependingOnInformation(insert, additionalColumnValues);
additionalColumnValues.putAll(insert.getAdditionalValues());
addDependingOnInformation(action, additionalColumnValues);
additionalColumnValues.putAll(action.getAdditionalValues());
return additionalColumnValues;
}
private <T> void addDependingOnInformation(Insert<T> insert, Map<String, Object> additionalColumnValues) {
private <T> void addDependingOnInformation(DbAction.WithDependingOn<T> action, Map<String, Object> additionalColumnValues) {
DbAction dependingOn = insert.getDependingOn();
if (dependingOn == null) {
return;
}
DbAction.WithEntity<?> dependingOn = action.getDependingOn();
RelationalPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(dependingOn.getEntityType());
String columnName = getColumnNameForReverseColumn(insert, persistentEntity);
String columnName = getColumnNameForReverseColumn(action);
Object identifier = getIdFromEntityDependingOn(dependingOn, persistentEntity);
@@ -105,16 +125,13 @@ class DefaultJdbcInterpreter implements Interpreter {
}
@Nullable
private Object getIdFromEntityDependingOn(DbAction dependingOn, RelationalPersistentEntity<?> persistentEntity) {
private Object getIdFromEntityDependingOn(DbAction.WithEntity<?> dependingOn, RelationalPersistentEntity<?> persistentEntity) {
return persistentEntity.getIdentifierAccessor(dependingOn.getEntity()).getIdentifier();
}
private <T> String getColumnNameForReverseColumn(Insert<T> insert, RelationalPersistentEntity<?> persistentEntity) {
private String getColumnNameForReverseColumn(DbAction.WithPropertyPath<?> action) {
PropertyPath path = insert.getPropertyPath().getPath();
Assert.notNull(path, "There shouldn't be an insert depending on another insert without having a PropertyPath.");
return persistentEntity.getRequiredPersistentProperty(path.getSegment()).getReverseColumnName();
PersistentPropertyPath<RelationalPersistentProperty> path = action.getPropertyPath();
return path.getRequiredLeafProperty().getReverseColumnName();
}
}

View File

@@ -17,7 +17,7 @@ package org.springframework.data.jdbc.core;
import java.util.Map;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.util.Assert;
@@ -38,12 +38,12 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy {
}
@Override
public <S> void update(S instance, Class<S> domainType) {
delegate.update(instance, domainType);
public <S> boolean update(S instance, Class<S> domainType) {
return delegate.update(instance, domainType);
}
@Override
public void delete(Object rootId, PropertyPath propertyPath) {
public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
delegate.delete(rootId, propertyPath);
}
@@ -58,7 +58,7 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy {
}
@Override
public void deleteAll(PropertyPath propertyPath) {
public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
delegate.deleteAll(propertyPath);
}

View File

@@ -25,8 +25,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -61,7 +61,8 @@ class SqlGenerator {
private final Lazy<String> deleteByListSql = Lazy.of(this::createDeleteByListSql);
private final SqlGeneratorSource sqlGeneratorSource;
SqlGenerator(RelationalMappingContext context, RelationalPersistentEntity<?> entity, SqlGeneratorSource sqlGeneratorSource) {
SqlGenerator(RelationalMappingContext context, RelationalPersistentEntity<?> entity,
SqlGeneratorSource sqlGeneratorSource) {
this.context = context;
this.entity = entity;
@@ -109,8 +110,8 @@ class SqlGenerator {
*
* @param columnName name of the column of the FK back to the referencing entity.
* @param keyColumn if the property is of type {@link Map} this column contains the map key.
* @param ordered whether the SQL statement should include an ORDER BY for the keyColumn. If this is {@code true},
* the keyColumn must not be {@code null}.
* @param ordered whether the SQL statement should include an ORDER BY for the keyColumn. If this is {@code true}, the
* keyColumn must not be {@code null}.
* @return a SQL String.
*/
String getFindAllByProperty(String columnName, @Nullable String keyColumn, boolean ordered) {
@@ -280,20 +281,20 @@ class SqlGenerator {
return String.format("DELETE FROM %s WHERE %s = :id", entity.getTableName(), entity.getIdColumn());
}
String createDeleteAllSql(@Nullable PropertyPath path) {
String createDeleteAllSql(@Nullable PersistentPropertyPath<RelationalPersistentProperty> path) {
if (path == null) {
return String.format("DELETE FROM %s", entity.getTableName());
}
RelationalPersistentEntity<?> entityToDelete = context.getRequiredPersistentEntity(path.getLeafType());
RelationalPersistentEntity<?> entityToDelete = context
.getRequiredPersistentEntity(path.getRequiredLeafProperty().getActualType());
RelationalPersistentEntity<?> owningEntity = context.getRequiredPersistentEntity(path.getOwningType());
RelationalPersistentProperty property = owningEntity.getRequiredPersistentProperty(path.getSegment());
RelationalPersistentProperty property = path.getBaseProperty();
String innerMostCondition = String.format("%s IS NOT NULL", property.getReverseColumnName());
String condition = cascadeConditions(innerMostCondition, path.next());
String condition = cascadeConditions(innerMostCondition, getSubPath(path));
return String.format("DELETE FROM %s WHERE %s", entityToDelete.getTableName(), condition);
}
@@ -302,29 +303,42 @@ class SqlGenerator {
return String.format("DELETE FROM %s WHERE %s IN (:ids)", entity.getTableName(), entity.getIdColumn());
}
String createDeleteByPath(PropertyPath path) {
String createDeleteByPath(PersistentPropertyPath<RelationalPersistentProperty> path) {
RelationalPersistentEntity<?> entityToDelete = context.getRequiredPersistentEntity(path.getLeafType());
RelationalPersistentEntity<?> owningEntity = context.getRequiredPersistentEntity(path.getOwningType());
RelationalPersistentProperty property = owningEntity.getRequiredPersistentProperty(path.getSegment());
RelationalPersistentEntity<?> entityToDelete = context
.getRequiredPersistentEntity(path.getRequiredLeafProperty().getActualType());
RelationalPersistentProperty property = path.getBaseProperty();
String innerMostCondition = String.format("%s = :rootId", property.getReverseColumnName());
String condition = cascadeConditions(innerMostCondition, path.next());
String condition = cascadeConditions(innerMostCondition, getSubPath(path));
return String.format("DELETE FROM %s WHERE %s", entityToDelete.getTableName(), condition);
}
private String cascadeConditions(String innerCondition, @Nullable PropertyPath path) {
private PersistentPropertyPath<RelationalPersistentProperty> getSubPath(
PersistentPropertyPath<RelationalPersistentProperty> path) {
if (path == null) {
int pathLength = path.getLength();
PersistentPropertyPath<RelationalPersistentProperty> ancestor = path;
for (int i = pathLength - 1; i > 0; i--) {
ancestor = path.getParentPath();
}
return path.getExtensionForBaseOf(ancestor);
}
private String cascadeConditions(String innerCondition, PersistentPropertyPath<RelationalPersistentProperty> path) {
if (path.getLength() == 0) {
return innerCondition;
}
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(path.getOwningType());
RelationalPersistentProperty property = entity.getPersistentProperty(path.getSegment());
Assert.notNull(property, "could not find property for path " + path.getSegment() + " in " + entity);
RelationalPersistentEntity<?> entity = context
.getRequiredPersistentEntity(path.getBaseProperty().getOwner().getTypeInformation());
RelationalPersistentProperty property = path.getRequiredLeafProperty();
return String.format("%s IN (SELECT %s FROM %s WHERE %s)", //
property.getReverseColumnName(), //

View File

@@ -27,6 +27,7 @@ import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.core.DefaultDataAccessStrategy;
import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy;
import org.springframework.data.jdbc.core.SqlGeneratorSource;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
@@ -130,10 +131,10 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
}
@Override
public <S> void update(S instance, Class<S> domainType) {
public <S> boolean update(S instance, Class<S> domainType) {
sqlSession().update(namespace(domainType) + ".update",
new MyBatisContext(null, instance, domainType, Collections.emptyMap()));
return sqlSession().update(namespace(domainType) + ".update",
new MyBatisContext(null, instance, domainType, Collections.emptyMap())) != 0;
}
@Override
@@ -144,10 +145,11 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
}
@Override
public void delete(Object rootId, PropertyPath propertyPath) {
public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
sqlSession().delete(namespace(propertyPath.getOwningType().getType()) + ".delete-" + toDashPath(propertyPath),
new MyBatisContext(rootId, null, propertyPath.getLeafProperty().getTypeInformation().getType(),
sqlSession().delete(
namespace(propertyPath.getBaseProperty().getOwner().getType()) + ".delete-" + toDashPath(propertyPath),
new MyBatisContext(rootId, null, propertyPath.getRequiredLeafProperty().getTypeInformation().getType(),
Collections.emptyMap()));
}
@@ -161,10 +163,10 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
}
@Override
public void deleteAll(PropertyPath propertyPath) {
public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
Class<?> baseType = propertyPath.getOwningType().getType();
Class<?> leafType = propertyPath.getLeafProperty().getTypeInformation().getType();
Class<?> baseType = propertyPath.getBaseProperty().getOwner().getType();
Class<?> leafType = propertyPath.getRequiredLeafProperty().getTypeInformation().getType();
sqlSession().delete( //
namespace(baseType) + ".deleteAll-" + toDashPath(propertyPath), //
@@ -217,7 +219,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
return this.sqlSession;
}
private String toDashPath(PropertyPath propertyPath) {
private String toDashPath(PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
return propertyPath.toDotPath().replaceAll("\\.", "-");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2018 the original author or authors.
* Copyright 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.
@@ -15,131 +15,36 @@
*/
package org.springframework.data.relational.core.conversion;
import lombok.Getter;
import lombok.ToString;
import lombok.NonNull;
import lombok.Value;
import java.util.HashMap;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
/**
* Abstracts over a single interaction with a database.
* An instance of this interface represents a (conceptual) single interaction with a database, e.g. a single update,
* used as a step when synchronizing the state of an aggregate with the database.
*
* @param <T> the type of the entity that is affected by this action.
* @author Jens Schauder
* @since 1.0
*/
@ToString
@Getter
public abstract class DbAction<T> {
public interface DbAction<T> {
/**
* {@link Class} of the entity of which the database representation is affected by this action.
*/
final Class<T> entityType;
/**
* The entity of which the database representation is affected by this action. Might be {@literal null}.
*/
private final T entity;
/**
* The path from the Aggregate Root to the entity affected by this {@link DbAction}.
*/
private final RelationalPropertyPath propertyPath;
/**
* Key-value-pairs to specify additional values to be used with the statement which can't be obtained from the entity,
* nor from {@link DbAction}s {@literal this} depends on. A used case are map keys, which need to be persisted with
* the map value but aren't part of the value.
*/
private final Map<String, Object> additionalValues = new HashMap<>();
/**
* Another action, this action depends on. For example the insert for one entity might need the id of another entity,
* which gets insert before this one. That action would be referenced by this property, so that the id becomes
* available at execution time. Might be {@literal null}.
*/
private final DbAction dependingOn;
private DbAction(Class<T> entityType, @Nullable T entity, @Nullable RelationalPropertyPath propertyPath,
@Nullable DbAction dependingOn) {
Assert.notNull(entityType, "entityType must not be null");
this.entityType = entityType;
this.entity = entity;
this.propertyPath = propertyPath;
this.dependingOn = dependingOn;
}
/**
* Creates an {@link Insert} action for the given parameters.
*
* @param entity the entity to insert into the database. Must not be {@code null}.
* @param propertyPath property path from the aggregate root to the entity to be saved. Must not be {@code null}.
* @param dependingOn a {@link DbAction} the to be created insert may depend on. Especially the insert of a parent
* entity. May be {@code null}.
* @param <T> the type of the entity to be inserted.
* @return a {@link DbAction} representing the insert.
*/
public static <T> Insert<T> insert(T entity, RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) {
return new Insert<>(entity, propertyPath, dependingOn);
}
/**
* Creates an {@link Update} action for the given parameters.
*
* @param entity the entity to update in the database. Must not be {@code null}.
* @param propertyPath property path from the aggregate root to the entity to be saved. Must not be {@code null}.
* @param dependingOn a {@link DbAction} the to be created update may depend on. Especially the insert of a parent
* entity. May be {@code null}.
* @param <T> the type of the entity to be updated.
* @return a {@link DbAction} representing the update.
*/
public static <T> Update<T> update(T entity, RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) {
return new Update<>(entity, propertyPath, dependingOn);
}
/**
* Creates a {@link Delete} action for the given parameters.
*
* @param id the id of the aggregate root for which referenced entities to be deleted. May not be {@code null}.
* @param type the type of the entity to be deleted. Must not be {@code null}.
* @param entity the aggregate roo to be deleted. May be {@code null}.
* @param propertyPath the property path from the aggregate root to the entity to be deleted.
* @param dependingOn an action this action might depend on.
* @param <T> the type of the entity to be deleted.
* @return a {@link DbAction} representing the deletion of the entity with given type and id.
*/
public static <T> Delete<T> delete(Object id, Class<T> type, @Nullable T entity,
@Nullable RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) {
return new Delete<>(id, type, entity, propertyPath, dependingOn);
}
/**
* Creates a {@link DeleteAll} action for the given parameters.
*
* @param type the type of entities to be deleted. May be {@code null}.
* @param propertyPath the property path describing the relation of the entity to be deleted to the aggregate it
* belongs to. May be {@code null} for the aggregate root.
* @param dependingOn an action this action might depend on. May be {@code null}.
* @param <T> the type of the entity to be deleted.
* @return a {@link DbAction} representing the deletion of all entities of a given type belonging to a specific type
* of aggregate root.
*/
public static <T> DeleteAll<T> deleteAll(Class<T> type, @Nullable RelationalPropertyPath propertyPath,
@Nullable DbAction dependingOn) {
return new DeleteAll<>(type, propertyPath, dependingOn);
}
Class<T> getEntityType();
/**
* Executing this DbAction with the given {@link Interpreter}.
* <p>
* The default implementation just performs exception handling and delegates to {@link #doExecuteWith(Interpreter)}.
*
* @param interpreter the {@link Interpreter} responsible for actually executing the {@link DbAction}.
* @param interpreter the {@link Interpreter} responsible for actually executing the {@link DbAction}.Must not be
* {@code null}.
*/
void executeWith(Interpreter interpreter) {
default void executeWith(Interpreter interpreter) {
try {
doExecuteWith(interpreter);
@@ -153,99 +58,240 @@ public abstract class DbAction<T> {
*
* @param interpreter the {@link Interpreter} responsible for actually executing the {@link DbAction}.
*/
protected abstract void doExecuteWith(Interpreter interpreter);
void doExecuteWith(Interpreter interpreter);
/**
* {@link InsertOrUpdate} must reference an entity.
* Represents an insert statement for a single entity that is not the root of an aggregate.
*
* @param <T> type o the entity for which this represents a database interaction
* @param <T> type of the entity for which this represents a database interaction.
*/
abstract static class InsertOrUpdate<T> extends DbAction<T> {
@Value
class Insert<T> implements WithDependingOn<T>, WithEntity<T> {
@SuppressWarnings("unchecked")
InsertOrUpdate(T entity, RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) {
super((Class<T>) entity.getClass(), entity, propertyPath, dependingOn);
@NonNull T entity;
@NonNull PersistentPropertyPath<RelationalPersistentProperty> propertyPath;
@NonNull WithEntity<?> dependingOn;
Map<String, Object> additionalValues = new HashMap<>();
@Override
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
}
@Override
public Class<T> getEntityType() {
return WithDependingOn.super.getEntityType();
}
}
/**
* Represents an insert statement.
* Represents an insert statement for the root of an aggregate.
*
* @param <T> type o the entity for which this represents a database interaction
* @param <T> type of the entity for which this represents a database interaction.
*/
public static class Insert<T> extends InsertOrUpdate<T> {
@Value
class InsertRoot<T> implements WithEntity<T> {
private Insert(T entity, RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) {
super(entity, propertyPath, dependingOn);
}
@NonNull T entity;
@Override
protected void doExecuteWith(Interpreter interpreter) {
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
}
}
/**
* Represents an update statement.
* Represents an update statement for a single entity that is not the root of an aggregate.
*
* @param <T> type o the entity for which this represents a database interaction
* @param <T> type of the entity for which this represents a database interaction.
*/
public static class Update<T> extends InsertOrUpdate<T> {
@Value
class Update<T> implements WithEntity<T> {
private Update(T entity, RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) {
super(entity, propertyPath, dependingOn);
}
@NonNull T entity;
@NonNull PersistentPropertyPath<RelationalPersistentProperty> propertyPath;
@Override
protected void doExecuteWith(Interpreter interpreter) {
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
}
}
/**
* Represents an delete statement, possibly a cascading delete statement, i.e. the delete necessary because one
* aggregate root gets deleted.
* Represents an insert statement for the root of an aggregate.
*
* @param <T> type o the entity for which this represents a database interaction
* @param <T> type of the entity for which this represents a database interaction.
*/
@Getter
public static class Delete<T> extends DbAction<T> {
@Value
class UpdateRoot<T> implements WithEntity<T> {
@NonNull private final T entity;
@Override
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
}
}
/**
* Represents a merge statement for a single entity that is not the root of an aggregate.
*
* @param <T> type of the entity for which this represents a database interaction.
*/
@Value
class Merge<T> implements WithDependingOn<T>, WithPropertyPath<T> {
@NonNull T entity;
@NonNull PersistentPropertyPath<RelationalPersistentProperty> propertyPath;
@NonNull WithEntity<?> dependingOn;
Map<String, Object> additionalValues = new HashMap<>();
@Override
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
}
}
/**
* Represents a delete statement for all entities that that a reachable via a give path from the aggregate root.
*
* @param <T> type of the entity for which this represents a database interaction.
*/
@Value
class Delete<T> implements WithPropertyPath<T> {
@NonNull Object rootId;
@NonNull PersistentPropertyPath<RelationalPersistentProperty> propertyPath;
@Override
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
}
}
/**
* Represents a delete statement for a aggregate root.
* <p>
* Note that deletes for contained entities that reference the root are to be represented by separate
* {@link DbAction}s.
*
* @param <T> type of the entity for which this represents a database interaction.
*/
@Value
class DeleteRoot<T> implements DbAction<T> {
@NonNull Class<T> entityType;
@NonNull Object rootId;
@Override
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
}
}
/**
* Represents an delete statement for all entities that that a reachable via a give path from any aggregate root of a
* given type.
*
* @param <T> type of the entity for which this represents a database interaction.
*/
@Value
class DeleteAll<T> implements WithPropertyPath<T> {
@NonNull PersistentPropertyPath<RelationalPersistentProperty> propertyPath;
@Override
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
}
}
/**
* Represents a delete statement for all aggregate roots of a given type.
* <p>
* Note that deletes for contained entities that reference the root are to be represented by separate
* {@link DbAction}s.
*
* @param <T> type of the entity for which this represents a database interaction.
*/
@Value
class DeleteAllRoot<T> implements DbAction<T> {
@NonNull private final Class<T> entityType;
@Override
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
}
}
/**
* An action depending on another action for providing additional information like the id of a parent entity.
*
* @author Jens Schauder
* @since 1.0
*/
interface WithDependingOn<T> extends WithPropertyPath<T> {
/**
* Id of the root for which all via {@link #propertyPath} referenced entities shall get deleted
* The {@link DbAction} of a parent entity, possibly the aggregate root. This is used to obtain values needed to
* persist the entity, that are not part of the current entity, especially the id of the parent, which might only
* become available once the parent entity got persisted.
*
* @return Guaranteed to be not {@code null}.
* @see #getAdditionalValues()
*/
private final Object rootId;
WithEntity<?> getDependingOn();
private Delete(@Nullable Object rootId, Class<T> type, @Nullable T entity, @Nullable RelationalPropertyPath propertyPath,
@Nullable DbAction dependingOn) {
/**
* Additional values to be set during insert or update statements.
* <p>
* Values come from parent entities but one might also add values manually.
*
* @return Guaranteed to be not {@code null}.
*/
Map<String, Object> getAdditionalValues();
}
super(type, entity, propertyPath, dependingOn);
/**
* A {@link DbAction} that stores the information of a single entity in the database.
*
* @author Jens Schauder
* @since 1.0
*/
interface WithEntity<T> extends DbAction<T> {
Assert.notNull(rootId, "rootId must not be null.");
this.rootId = rootId;
}
/**
* @return the entity to persist. Guaranteed to be not {@code null}.
*/
T getEntity();
@SuppressWarnings("unchecked")
@Override
protected void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
default Class<T> getEntityType() {
return (Class<T>) getEntity().getClass();
}
}
/**
* Represents an delete statement.
* A {@link DbAction} not operation on the root of an aggregate but on its contained entities.
*
* @param <T> type o the entity for which this represents a database interaction
* @author Jens Schauder
* @since 1.0
*/
public static class DeleteAll<T> extends DbAction<T> {
interface WithPropertyPath<T> extends DbAction<T> {
private DeleteAll(Class<T> entityType, @Nullable RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) {
super(entityType, null, propertyPath, dependingOn);
}
/**
* @return the path from the aggregate root to the affected entity
*/
PersistentPropertyPath<RelationalPersistentProperty> getPropertyPath();
@SuppressWarnings("unchecked")
@Override
protected void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
default Class<T> getEntityType() {
return (Class<T>) getPropertyPath().getRequiredLeafProperty().getActualType();
}
}
}

View File

@@ -29,13 +29,6 @@ public class DbActionExecutionException extends RuntimeException {
* @param cause the underlying exception. May not be {@code null}.
*/
public DbActionExecutionException(DbAction<?> action, Throwable cause) {
super( //
String.format("Failed to execute %s for instance %s of type %s with path %s", //
action.getClass().getSimpleName(), //
action.getEntity(), //
action.getEntityType(), //
action.getPropertyPath() == null ? "" : action.getPropertyPath().toDotPath() //
), //
cause);
super("Failed to execute " + action, cause);
}
}

View File

@@ -17,8 +17,13 @@ package org.springframework.data.relational.core.conversion;
import org.springframework.data.relational.core.conversion.DbAction.Delete;
import org.springframework.data.relational.core.conversion.DbAction.DeleteAll;
import org.springframework.data.relational.core.conversion.DbAction.DeleteAllRoot;
import org.springframework.data.relational.core.conversion.DbAction.DeleteRoot;
import org.springframework.data.relational.core.conversion.DbAction.Insert;
import org.springframework.data.relational.core.conversion.DbAction.InsertRoot;
import org.springframework.data.relational.core.conversion.DbAction.Merge;
import org.springframework.data.relational.core.conversion.DbAction.Update;
import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot;
/**
* An {@link Interpreter} gets called by a {@link AggregateChange} for each {@link DbAction} and is tasked with
@@ -31,6 +36,10 @@ import org.springframework.data.relational.core.conversion.DbAction.Update;
*/
public interface Interpreter {
<T> void interpret(Insert<T> insert);
<T> void interpret(InsertRoot<T> insert);
/**
* Interpret an {@link Update}. Interpreting normally means "executing".
*
@@ -39,9 +48,15 @@ public interface Interpreter {
*/
<T> void interpret(Update<T> update);
<T> void interpret(Insert<T> insert);
<T> void interpret(UpdateRoot<T> update);
<T> void interpret(Merge<T> update);
<T> void interpret(Delete<T> delete);
<T> void interpret(DeleteRoot<T> deleteRoot);
<T> void interpret(DeleteAll<T> delete);
<T> void interpret(DeleteAllRoot<T> DeleteAllRoot);
}

View File

@@ -15,8 +15,15 @@
*/
package org.springframework.data.relational.core.conversion;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.data.convert.EntityWriter;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Converts an entity that is about to be deleted into {@link DbAction}s inside a {@link AggregateChange} that need to
@@ -25,10 +32,15 @@ import org.springframework.lang.Nullable;
* @author Jens Schauder
* @since 1.0
*/
public class RelationalEntityDeleteWriter extends RelationalEntityWriterSupport {
public class RelationalEntityDeleteWriter implements EntityWriter<Object, AggregateChange<?>> {
private final RelationalMappingContext context;
public RelationalEntityDeleteWriter(RelationalMappingContext context) {
super(context);
Assert.notNull(context, "Context must not be null");
this.context = context;
}
/**
@@ -51,22 +63,41 @@ public class RelationalEntityDeleteWriter extends RelationalEntityWriterSupport
private void deleteAll(AggregateChange<?> aggregateChange) {
context.referencedEntities(aggregateChange.getEntityType(), null)
.forEach(p -> aggregateChange.addAction(DbAction.deleteAll(p.getLeafType(), new RelationalPropertyPath(p), null)));
List<DbAction> actions = new ArrayList<>();
aggregateChange.addAction(DbAction.deleteAll(aggregateChange.getEntityType(), null, null));
context.findPersistentPropertyPaths(aggregateChange.getEntityType(), PersistentProperty::isEntity)
.forEach(p -> actions.add(new DbAction.DeleteAll<>(p)));
Collections.reverse(actions);
actions.forEach(aggregateChange::addAction);
DbAction.DeleteAllRoot<?> result = new DbAction.DeleteAllRoot<>(aggregateChange.getEntityType());
aggregateChange.addAction(result);
}
private <T> void deleteById(Object id, AggregateChange<T> aggregateChange) {
deleteReferencedEntities(id, aggregateChange);
aggregateChange.addAction(DbAction.delete( //
id, //
aggregateChange.getEntityType(), //
aggregateChange.getEntity(), //
null, //
null //
));
aggregateChange.addAction(new DbAction.DeleteRoot<>(aggregateChange.getEntityType(), id));
}
/**
* add {@link DbAction.Delete} actions to the {@link AggregateChange} for deleting all referenced entities.
*
* @param id id of the aggregate root, of which the referenced entities get deleted.
* @param aggregateChange the change object to which the actions should get added. Must not be {@code null}
*/
private void deleteReferencedEntities(Object id, AggregateChange<?> aggregateChange) {
List<DbAction> actions = new ArrayList<>();
context.findPersistentPropertyPaths(aggregateChange.getEntityType(), PersistentProperty::isEntity)
.forEach(p -> actions.add(new DbAction.Delete<>(id, p)));
Collections.reverse(actions);
actions.forEach(aggregateChange::addAction);
}
}

View File

@@ -15,209 +15,240 @@
*/
package org.springframework.data.relational.core.conversion;
import lombok.Data;
import lombok.NonNull;
import lombok.Value;
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 java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.convert.EntityWriter;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.relational.core.conversion.DbAction.Insert;
import org.springframework.data.relational.core.conversion.DbAction.Update;
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.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.util.StreamUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Converts an entity that is about to be saved into {@link DbAction}s inside a {@link AggregateChange} that need to be
* executed against the database to recreate the appropriate state in the database.
* Converts an aggregate represented by its root into an {@link AggregateChange}.
*
* @author Jens Schauder
* @since 1.0
*/
public class RelationalEntityWriter extends RelationalEntityWriterSupport {
public class RelationalEntityWriter implements EntityWriter<Object, AggregateChange<?>> {
private final RelationalMappingContext context;
public RelationalEntityWriter(RelationalMappingContext context) {
super(context);
this.context = context;
}
/**
* Converts an aggregate represented by its aggregate root into a list of {@link DbAction}s and adds them to the
* {@link AggregateChange} passed in as an argument.
*
* @param aggregateRoot the aggregate root to be written to the {@link AggregateChange} Must not be {@code null}.
* @param aggregateChange the {@link AggregateChange} to which the {@link DbAction}s get written.
*/
@Override
public void write(Object aggregateRoot, AggregateChange aggregateChange) {
public void write(Object root, AggregateChange<?> aggregateChange) {
Class<?> type = aggregateRoot.getClass();
RelationalPropertyPath propertyPath = RelationalPropertyPath.from("", type);
new WritingContext(root, aggregateChange).write();
}
PersistentEntity<?, RelationalPersistentProperty> persistentEntity = context.getRequiredPersistentEntity(type);
/**
* Holds context information for the current write operation.
*/
private class WritingContext {
if (persistentEntity.isNew(aggregateRoot)) {
private final Object root;
private final AggregateChange<?> aggregateChange;
private final PersistentPropertyPaths<?, RelationalPersistentProperty> paths;
private final Map<PathNode, DbAction> previousActions = new HashMap<>();
private Map<PersistentPropertyPath<RelationalPersistentProperty>, List<PathNode>> nodesCache = new HashMap<>();
Insert<Object> insert = DbAction.insert(aggregateRoot, propertyPath, null);
WritingContext(Object root, AggregateChange<?> aggregateChange) {
this.root = root;
this.aggregateChange = aggregateChange;
paths = context.findPersistentPropertyPaths(aggregateChange.getEntityType(), PersistentProperty::isEntity);
}
private void write() {
if (isNew(root)) {
setRootAction(new DbAction.InsertRoot<>(aggregateChange.getEntity()));
insertReferenced();
} else {
deleteReferenced();
setRootAction(new DbAction.UpdateRoot<>(aggregateChange.getEntity()));
insertReferenced();
}
}
//// Operations on all paths
private void insertReferenced() {
paths.forEach(this::insertAll);
}
private void insertAll(PersistentPropertyPath<RelationalPersistentProperty> path) {
from(path).forEach(node -> {
DbAction.Insert<Object> insert;
if (node.path.getRequiredLeafProperty().isQualified()) {
KeyValue value = (KeyValue) node.getValue();
insert = new DbAction.Insert<>(value.value, path, getAction(node.parent));
insert.getAdditionalValues().put(node.path.getRequiredLeafProperty().getKeyColumn(), value.key);
} else {
insert = new DbAction.Insert<>(node.getValue(), path, getAction(node.parent));
}
previousActions.put(node, insert);
aggregateChange.addAction(insert);
});
}
private void deleteReferenced() {
ArrayList<DbAction> deletes = new ArrayList<>();
paths.forEach(path -> deletes.add(0, deleteReferenced(path)));
deletes.forEach(aggregateChange::addAction);
}
/// Operations on a single path
private DbAction.Delete<?> deleteReferenced(PersistentPropertyPath<RelationalPersistentProperty> path) {
Object id = context.getRequiredPersistentEntity(aggregateChange.getEntityType())
.getIdentifierAccessor(aggregateChange.getEntity()).getIdentifier();
return new DbAction.Delete<>(id, path);
}
//// methods not directly related to the creation of DbActions
private void setRootAction(DbAction<?> insert) {
previousActions.put(null, insert);
aggregateChange.addAction(insert);
referencedEntities(aggregateRoot) //
.forEach( //
propertyAndValue -> //
insertReferencedEntities( //
propertyAndValue, //
aggregateChange, //
propertyPath.nested(propertyAndValue.property.getName()), //
insert) //
);
} else {
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(type);
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(aggregateRoot);
deleteReferencedEntities(identifierAccessor.getRequiredIdentifier(), aggregateChange);
Update<Object> update = DbAction.update(aggregateRoot, propertyPath, null);
aggregateChange.addAction(update);
referencedEntities(aggregateRoot).forEach(propertyAndValue -> insertReferencedEntities(propertyAndValue,
aggregateChange, propertyPath.nested(propertyAndValue.property.getName()), update));
}
}
private void insertReferencedEntities(PropertyAndValue propertyAndValue, AggregateChange aggregateChange,
RelationalPropertyPath propertyPath, DbAction dependingOn) {
Insert<Object> insert;
if (propertyAndValue.property.isQualified()) {
KeyValue valueAsEntry = (KeyValue) propertyAndValue.value;
insert = DbAction.insert(valueAsEntry.getValue(), propertyPath, dependingOn);
insert.getAdditionalValues().put(propertyAndValue.property.getKeyColumn(), valueAsEntry.getKey());
} else {
insert = DbAction.insert(propertyAndValue.value, propertyPath, dependingOn);
}
aggregateChange.addAction(insert);
referencedEntities(insert.getEntity()) //
.forEach(pav -> insertReferencedEntities( //
pav, //
aggregateChange, //
propertyPath.nested(pav.property.getName()), //
dependingOn) //
);
}
@Nullable
private DbAction.WithEntity<?> getAction(@Nullable PathNode parent) {
private Stream<PropertyAndValue> referencedEntities(Object o) {
RelationalPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(o.getClass());
return StreamUtils.createStreamFromIterator(persistentEntity.iterator()) //
.filter(PersistentProperty::isEntity) //
.flatMap( //
p -> referencedEntity(p, persistentEntity.getPropertyAccessor(o)) //
.map(e -> new PropertyAndValue(p, e)) //
);
}
private Stream<Object> referencedEntity(RelationalPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
Class<?> actualType = p.getActualType();
RelationalPersistentEntity<?> persistentEntity = context //
.getPersistentEntity(actualType);
if (persistentEntity == null) {
return Stream.empty();
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;
}
Class<?> type = p.getType();
if (List.class.isAssignableFrom(type)) {
return listPropertyAsStream(p, propertyAccessor);
private boolean isNew(Object o) {
return context.getRequiredPersistentEntity(o.getClass()).isNew(o);
}
if (Collection.class.isAssignableFrom(type)) {
return collectionPropertyAsStream(p, propertyAccessor);
private List<WritingContext.PathNode> from(PersistentPropertyPath<RelationalPersistentProperty> path) {
List<WritingContext.PathNode> nodes = new ArrayList<>();
if (path.getLength() == 1) {
Object value = context //
.getRequiredPersistentEntity(aggregateChange.getEntityType()) //
.getPropertyAccessor(aggregateChange.getEntity()) //
.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.value)
.getProperty(path.getRequiredLeafProperty());
nodes.addAll(createNodes(path, parentNode, value));
});
}
nodesCache.put(path, nodes);
return nodes;
}
if (Map.class.isAssignableFrom(type)) {
return mapPropertyAsStream(p, propertyAccessor);
private List<PathNode> createNodes(PersistentPropertyPath<RelationalPersistentProperty> path,
@Nullable PathNode parentNode, @Nullable Object value) {
if (value == null) {
return Collections.emptyList();
}
List<WritingContext.PathNode> nodes = new ArrayList<>();
if (path.getRequiredLeafProperty().isQualified()) {
if (path.getRequiredLeafProperty().isMap()) {
((Map<?, ?>) value).forEach((k, v) -> nodes.add(new PathNode(path, parentNode, new KeyValue(k, v))));
} else {
List listValue = (List) value;
for (int k = 0; k < listValue.size(); k++) {
nodes.add(new PathNode(path, parentNode, new KeyValue(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;
}
return singlePropertyAsStream(p, propertyAccessor);
}
/**
* 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.
*/
private class PathNode {
@SuppressWarnings("unchecked")
private Stream<Object> collectionPropertyAsStream(RelationalPersistentProperty p,
PersistentPropertyAccessor propertyAccessor) {
private final PersistentPropertyPath<RelationalPersistentProperty> path;
@Nullable private final PathNode parent;
private final Object value;
Object property = propertyAccessor.getProperty(p);
private PathNode(PersistentPropertyPath<RelationalPersistentProperty> path, @Nullable PathNode parent,
Object value) {
return property == null //
? Stream.empty() //
: ((Collection<Object>) property).stream();
}
this.path = path;
this.parent = parent;
this.value = value;
}
@SuppressWarnings("unchecked")
private Stream<Object> listPropertyAsStream(RelationalPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
public Object getValue() {
return value;
}
Object property = propertyAccessor.getProperty(p);
if (property == null) {
return Stream.empty();
}
// ugly hackery since Java streams don't have a zip method.
AtomicInteger index = new AtomicInteger();
List<Object> listProperty = (List<Object>) property;
return listProperty.stream().map(e -> new KeyValue(index.getAndIncrement(), e));
}
@SuppressWarnings("unchecked")
private Stream<Object> mapPropertyAsStream(RelationalPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
Object property = propertyAccessor.getProperty(p);
return property == null //
? Stream.empty() //
: ((Map<Object, Object>) property).entrySet().stream().map(e -> new KeyValue(e.getKey(), e.getValue()));
}
private Stream<Object> singlePropertyAsStream(RelationalPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
Object property = propertyAccessor.getProperty(p);
if (property == null) {
return Stream.empty();
}
return Stream.of(property);
}
/**
* Holds key and value of a {@link Map.Entry} but without any ties to {@link Map} implementations.
*/
@Data
@Value
private static class KeyValue {
private final Object key;
private final Object value;
@NonNull Object key;
@NonNull Object value;
}
@Data
private static class PropertyAndValue {
private final RelationalPersistentProperty property;
private final Object value;
}
}

View File

@@ -1,50 +0,0 @@
/*
* 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.convert.EntityWriter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.util.Assert;
/**
* Common infrastructure needed by different implementations of {@link EntityWriter}<Object, AggregateChange>.
*
* @author Jens Schauder
* @since 1.0
*/
abstract class RelationalEntityWriterSupport implements EntityWriter<Object, AggregateChange<?>> {
protected final RelationalMappingContext context;
RelationalEntityWriterSupport(RelationalMappingContext context) {
Assert.notNull(context, "Context must not be null");
this.context = context;
}
/**
* add {@link org.springframework.data.relational.core.conversion.DbAction.Delete} actions to the {@link AggregateChange}
* for deleting all referenced entities.
*
* @param id id of the aggregate root, of which the referenced entities get deleted.
* @param aggregateChange the change object to which the actions should get added. Must not be {@code null}
*/
void deleteReferencedEntities(Object id, AggregateChange<?> aggregateChange) {
context.referencedEntities(aggregateChange.getEntityType(), null).forEach(
p -> aggregateChange.addAction(DbAction.delete(id, p.getLeafType(), null, new RelationalPropertyPath(p), null)));
}
}

View File

@@ -65,32 +65,6 @@ public class RelationalMappingContext extends AbstractMappingContext<RelationalP
setSimpleTypeHolder(new SimpleTypeHolder(Collections.emptySet(), true));
}
/**
* returns all {@link PropertyPath}s reachable from the root type in the order needed for deleting, i.e. the deepest
* reference first.
*/
public List<PropertyPath> referencedEntities(Class<?> rootType, @Nullable PropertyPath path) {
List<PropertyPath> paths = new ArrayList<>();
Class<?> currentType = path == null ? rootType : path.getLeafType();
RelationalPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(currentType);
for (RelationalPersistentProperty property : persistentEntity) {
if (property.isEntity()) {
PropertyPath nextPath = path == null ? PropertyPath.from(property.getName(), rootType)
: path.nested(property.getName());
paths.add(nextPath);
paths.addAll(referencedEntities(rootType, nextPath));
}
}
Collections.reverse(paths);
return paths;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation)