From bdefd3da9a5063fdd55c663f8f17be3930fde96a Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 12 Jun 2018 14:24:20 +0200 Subject: [PATCH] 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. --- .../core/CascadingDataAccessStrategy.java | 10 +- .../data/jdbc/core/DataAccessStrategy.java | 11 +- .../jdbc/core/DefaultDataAccessStrategy.java | 23 +- .../jdbc/core/DefaultJdbcInterpreter.java | 81 ++-- .../core/DelegatingDataAccessStrategy.java | 10 +- .../data/jdbc/core/SqlGenerator.java | 54 ++- .../mybatis/MyBatisDataAccessStrategy.java | 22 +- .../relational/core/conversion/DbAction.java | 356 ++++++++++-------- .../DbActionExecutionException.java | 9 +- .../core/conversion/Interpreter.java | 17 +- .../RelationalEntityDeleteWriter.java | 55 ++- .../conversion/RelationalEntityWriter.java | 337 +++++++++-------- .../RelationalEntityWriterSupport.java | 50 --- .../mapping/RelationalMappingContext.java | 26 -- .../core/DefaultJdbcInterpreterUnitTests.java | 12 +- .../MyBatisDataAccessStrategyUnitTests.java | 47 ++- .../data/jdbc/core/PropertyPathUtils.java | 42 +++ ...orContextBasedNamingStrategyUnitTests.java | 33 +- ...GeneratorFixedNamingStrategyUnitTests.java | 28 +- .../data/jdbc/core/SqlGeneratorUnitTests.java | 31 +- .../PersistentPropertyPathTestUtils.java | 41 ++ .../model/JdbcMappingContextUnitTests.java | 102 ----- ...ryManipulateDbActionsIntegrationTests.java | 10 +- .../core/conversion/DbActionUnitTests.java | 21 +- ...RelationalEntityDeleteWriterUnitTests.java | 28 +- .../RelationalEntityWriterUnitTests.java | 199 ++++++---- .../RelationalMappingContextUnitTests.java | 71 ---- ...ollectionsNoIdIntegrationTests-mariadb.sql | 2 + ...hCollectionsNoIdIntegrationTests-mysql.sql | 2 + ...llectionsNoIdIntegrationTests-postgres.sql | 4 + 30 files changed, 926 insertions(+), 808 deletions(-) delete mode 100644 src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterSupport.java create mode 100644 src/test/java/org/springframework/data/jdbc/core/PropertyPathUtils.java create mode 100644 src/test/java/org/springframework/data/jdbc/core/mapping/PersistentPropertyPathTestUtils.java delete mode 100644 src/test/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContextUnitTests.java delete mode 100644 src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java create mode 100644 src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-mariadb.sql create mode 100644 src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-mysql.sql create mode 100644 src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-postgres.sql diff --git a/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java index 311dbd9c..1bfbde3c 100644 --- a/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java @@ -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 void update(S instance, Class domainType) { - collectVoid(das -> das.update(instance, domainType)); + public boolean update(S instance, Class 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 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 propertyPath) { collectVoid(das -> das.deleteAll(propertyPath)); } diff --git a/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java index 8d45dd86..7c1175f3 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java @@ -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 the type of the instance to save. + * @return wether the update actually updated a row. */ - void update(T instance, Class domainType); + boolean update(T instance, Class 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 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 propertyPath); /** * Counts the rows in the table representing the given domain type. diff --git a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java index ac528ad0..639daf34 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -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 void update(S instance, Class domainType) { + public boolean update(S instance, Class domainType) { RelationalPersistentEntity 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 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 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 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())); } } diff --git a/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java b/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java index ef036bb1..a79de277 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java @@ -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 void interpret(InsertRoot insert) { + accessStrategy.insert(insert.getEntity(), insert.getEntityType(), new HashMap<>()); + } + @Override public void interpret(Update update) { accessStrategy.update(update.getEntity(), update.getEntityType()); } @Override - public void interpret(Delete delete) { + public void interpret(UpdateRoot 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 void interpret(Merge merge) { + + // temporary implementation + if (!accessStrategy.update(merge.getEntity(), merge.getEntityType())) { + accessStrategy.insert(merge.getEntity(), merge.getEntityType(), createAdditionalColumnValues(merge)); } } @Override - public void interpret(DeleteAll delete) { - - if (delete.getEntityType() == null) { - accessStrategy.deleteAll(delete.getPropertyPath().getPath()); - } else { - accessStrategy.deleteAll(delete.getEntityType()); - } + public void interpret(Delete delete) { + accessStrategy.delete(delete.getRootId(), delete.getPropertyPath()); } - private Map createAdditionalColumnValues(Insert insert) { + @Override + public void interpret(DeleteRoot delete) { + accessStrategy.delete(delete.getRootId(), delete.getEntityType()); + } + + @Override + public void interpret(DeleteAll delete) { + accessStrategy.deleteAll(delete.getPropertyPath()); + } + + @Override + public void interpret(DeleteAllRoot deleteAllRoot) { + accessStrategy.deleteAll(deleteAllRoot.getEntityType()); + } + + private Map createAdditionalColumnValues(DbAction.WithDependingOn action) { Map additionalColumnValues = new HashMap<>(); - addDependingOnInformation(insert, additionalColumnValues); - additionalColumnValues.putAll(insert.getAdditionalValues()); + addDependingOnInformation(action, additionalColumnValues); + additionalColumnValues.putAll(action.getAdditionalValues()); return additionalColumnValues; } - private void addDependingOnInformation(Insert insert, Map additionalColumnValues) { + private void addDependingOnInformation(DbAction.WithDependingOn action, Map 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 String getColumnNameForReverseColumn(Insert 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 path = action.getPropertyPath(); + return path.getRequiredLeafProperty().getReverseColumnName(); } } diff --git a/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java index e6c76610..bd074b72 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java @@ -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 void update(S instance, Class domainType) { - delegate.update(instance, domainType); + public boolean update(S instance, Class domainType) { + return delegate.update(instance, domainType); } @Override - public void delete(Object rootId, PropertyPath propertyPath) { + public void delete(Object rootId, PersistentPropertyPath propertyPath) { delegate.delete(rootId, propertyPath); } @@ -58,7 +58,7 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy { } @Override - public void deleteAll(PropertyPath propertyPath) { + public void deleteAll(PersistentPropertyPath propertyPath) { delegate.deleteAll(propertyPath); } diff --git a/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java b/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java index 15e56f60..d6e0b8e6 100644 --- a/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java +++ b/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java @@ -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 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 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 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 getSubPath( + PersistentPropertyPath path) { - if (path == null) { + int pathLength = path.getLength(); + + PersistentPropertyPath ancestor = path; + + for (int i = pathLength - 1; i > 0; i--) { + ancestor = path.getParentPath(); + } + + return path.getExtensionForBaseOf(ancestor); + } + + private String cascadeConditions(String innerCondition, PersistentPropertyPath 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(), // diff --git a/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 2973b8b1..0c577fd1 100644 --- a/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -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 void update(S instance, Class domainType) { + public boolean update(S instance, Class 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 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 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 propertyPath) { return propertyPath.toDotPath().replaceAll("\\.", "-"); } } diff --git a/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java b/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java index f21ca008..f4128dfd 100644 --- a/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java +++ b/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java @@ -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 the type of the entity that is affected by this action. * @author Jens Schauder * @since 1.0 */ -@ToString -@Getter -public abstract class DbAction { +public interface DbAction { - /** - * {@link Class} of the entity of which the database representation is affected by this action. - */ - final Class 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 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 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 the type of the entity to be inserted. - * @return a {@link DbAction} representing the insert. - */ - public static Insert 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 the type of the entity to be updated. - * @return a {@link DbAction} representing the update. - */ - public static Update 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 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 Delete delete(Object id, Class 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 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 DeleteAll deleteAll(Class type, @Nullable RelationalPropertyPath propertyPath, - @Nullable DbAction dependingOn) { - return new DeleteAll<>(type, propertyPath, dependingOn); - } + Class getEntityType(); /** * Executing this DbAction with the given {@link Interpreter}. + *

+ * 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 { * * @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 type o the entity for which this represents a database interaction + * @param type of the entity for which this represents a database interaction. */ - abstract static class InsertOrUpdate extends DbAction { + @Value + class Insert implements WithDependingOn, WithEntity { - @SuppressWarnings("unchecked") - InsertOrUpdate(T entity, RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) { - super((Class) entity.getClass(), entity, propertyPath, dependingOn); + @NonNull T entity; + @NonNull PersistentPropertyPath propertyPath; + @NonNull WithEntity dependingOn; + + Map additionalValues = new HashMap<>(); + + @Override + public void doExecuteWith(Interpreter interpreter) { + interpreter.interpret(this); + } + + @Override + public Class getEntityType() { + return WithDependingOn.super.getEntityType(); } } /** - * Represents an insert statement. + * Represents an insert statement for the root of an aggregate. * - * @param type o the entity for which this represents a database interaction + * @param type of the entity for which this represents a database interaction. */ - public static class Insert extends InsertOrUpdate { + @Value + class InsertRoot implements WithEntity { - 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 type o the entity for which this represents a database interaction + * @param type of the entity for which this represents a database interaction. */ - public static class Update extends InsertOrUpdate { + @Value + class Update implements WithEntity { - private Update(T entity, RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) { - super(entity, propertyPath, dependingOn); - } + @NonNull T entity; + @NonNull PersistentPropertyPath 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 type o the entity for which this represents a database interaction + * @param type of the entity for which this represents a database interaction. */ - @Getter - public static class Delete extends DbAction { + @Value + class UpdateRoot implements WithEntity { + + @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 type of the entity for which this represents a database interaction. + */ + @Value + class Merge implements WithDependingOn, WithPropertyPath { + + @NonNull T entity; + @NonNull PersistentPropertyPath propertyPath; + @NonNull WithEntity dependingOn; + + Map 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 type of the entity for which this represents a database interaction. + */ + @Value + class Delete implements WithPropertyPath { + + @NonNull Object rootId; + @NonNull PersistentPropertyPath propertyPath; + + @Override + public void doExecuteWith(Interpreter interpreter) { + interpreter.interpret(this); + } + } + + /** + * Represents a delete statement for a aggregate root. + *

+ * Note that deletes for contained entities that reference the root are to be represented by separate + * {@link DbAction}s. + * + * @param type of the entity for which this represents a database interaction. + */ + @Value + class DeleteRoot implements DbAction { + + @NonNull Class 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 type of the entity for which this represents a database interaction. + */ + @Value + class DeleteAll implements WithPropertyPath { + + @NonNull PersistentPropertyPath propertyPath; + + @Override + public void doExecuteWith(Interpreter interpreter) { + interpreter.interpret(this); + } + } + + /** + * Represents a delete statement for all aggregate roots of a given type. + *

+ * Note that deletes for contained entities that reference the root are to be represented by separate + * {@link DbAction}s. + * + * @param type of the entity for which this represents a database interaction. + */ + @Value + class DeleteAllRoot implements DbAction { + + @NonNull private final Class 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 extends WithPropertyPath { /** - * 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 type, @Nullable T entity, @Nullable RelationalPropertyPath propertyPath, - @Nullable DbAction dependingOn) { + /** + * Additional values to be set during insert or update statements. + *

+ * Values come from parent entities but one might also add values manually. + * + * @return Guaranteed to be not {@code null}. + */ + Map 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 extends DbAction { - 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 getEntityType() { + return (Class) getEntity().getClass(); } } /** - * Represents an delete statement. + * A {@link DbAction} not operation on the root of an aggregate but on its contained entities. * - * @param type o the entity for which this represents a database interaction + * @author Jens Schauder + * @since 1.0 */ - public static class DeleteAll extends DbAction { + interface WithPropertyPath extends DbAction { - private DeleteAll(Class entityType, @Nullable RelationalPropertyPath propertyPath, @Nullable DbAction dependingOn) { - super(entityType, null, propertyPath, dependingOn); - } + /** + * @return the path from the aggregate root to the affected entity + */ + PersistentPropertyPath getPropertyPath(); + @SuppressWarnings("unchecked") @Override - protected void doExecuteWith(Interpreter interpreter) { - interpreter.interpret(this); + default Class getEntityType() { + return (Class) getPropertyPath().getRequiredLeafProperty().getActualType(); } } } diff --git a/src/main/java/org/springframework/data/relational/core/conversion/DbActionExecutionException.java b/src/main/java/org/springframework/data/relational/core/conversion/DbActionExecutionException.java index cfccdf4e..345582bc 100644 --- a/src/main/java/org/springframework/data/relational/core/conversion/DbActionExecutionException.java +++ b/src/main/java/org/springframework/data/relational/core/conversion/DbActionExecutionException.java @@ -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); } } diff --git a/src/main/java/org/springframework/data/relational/core/conversion/Interpreter.java b/src/main/java/org/springframework/data/relational/core/conversion/Interpreter.java index b21fb535..7cd89a2b 100644 --- a/src/main/java/org/springframework/data/relational/core/conversion/Interpreter.java +++ b/src/main/java/org/springframework/data/relational/core/conversion/Interpreter.java @@ -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 { + void interpret(Insert insert); + + void interpret(InsertRoot insert); + /** * Interpret an {@link Update}. Interpreting normally means "executing". * @@ -39,9 +48,15 @@ public interface Interpreter { */ void interpret(Update update); - void interpret(Insert insert); + void interpret(UpdateRoot update); + + void interpret(Merge update); void interpret(Delete delete); + void interpret(DeleteRoot deleteRoot); + void interpret(DeleteAll delete); + + void interpret(DeleteAllRoot DeleteAllRoot); } diff --git a/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java b/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java index fdaaf121..7bd160b7 100644 --- a/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java +++ b/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java @@ -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> { + + 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 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 void deleteById(Object id, AggregateChange 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 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); } } diff --git a/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java b/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java index d6d1751f..3c94a7aa 100644 --- a/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java +++ b/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java @@ -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> { 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 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 paths; + private final Map previousActions = new HashMap<>(); + private Map, List> nodesCache = new HashMap<>(); - Insert 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 path) { + + from(path).forEach(node -> { + + DbAction.Insert 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 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 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 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 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 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 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 from(PersistentPropertyPath path) { + + List 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 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 createNodes(PersistentPropertyPath path, + @Nullable PathNode parentNode, @Nullable Object value) { + + if (value == null) { + return Collections.emptyList(); + } + + List nodes = new ArrayList<>(); + + if (path.getRequiredLeafProperty().isQualified()) { + + if (path.getRequiredLeafProperty().isMap()) { + ((Map) value).forEach((k, v) -> nodes.add(new PathNode(path, parentNode, 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 collectionPropertyAsStream(RelationalPersistentProperty p, - PersistentPropertyAccessor propertyAccessor) { + private final PersistentPropertyPath path; + @Nullable private final PathNode parent; + private final Object value; - Object property = propertyAccessor.getProperty(p); + private PathNode(PersistentPropertyPath path, @Nullable PathNode parent, + Object value) { - return property == null // - ? Stream.empty() // - : ((Collection) property).stream(); - } + this.path = path; + this.parent = parent; + this.value = value; + } - @SuppressWarnings("unchecked") - private Stream 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 listProperty = (List) property; - - return listProperty.stream().map(e -> new KeyValue(index.getAndIncrement(), e)); - } - - @SuppressWarnings("unchecked") - private Stream mapPropertyAsStream(RelationalPersistentProperty p, PersistentPropertyAccessor propertyAccessor) { - - Object property = propertyAccessor.getProperty(p); - - return property == null // - ? Stream.empty() // - : ((Map) property).entrySet().stream().map(e -> new KeyValue(e.getKey(), e.getValue())); - } - - private Stream 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; - } } diff --git a/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterSupport.java b/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterSupport.java deleted file mode 100644 index a338634d..00000000 --- a/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterSupport.java +++ /dev/null @@ -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}. - * - * @author Jens Schauder - * @since 1.0 - */ -abstract class RelationalEntityWriterSupport implements EntityWriter> { - 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))); - } -} diff --git a/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index 06f7c954..01ae09ce 100644 --- a/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -65,32 +65,6 @@ public class RelationalMappingContext extends AbstractMappingContext referencedEntities(Class rootType, @Nullable PropertyPath path) { - - List 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) diff --git a/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java index bce5a3de..cf82043a 100644 --- a/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java @@ -16,7 +16,7 @@ package org.springframework.data.jdbc.core; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import java.util.AbstractMap.SimpleEntry; @@ -25,13 +25,11 @@ import java.util.Map; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.data.annotation.Id; -import org.springframework.data.relational.core.conversion.DbAction; -import org.springframework.data.relational.core.conversion.RelationalPropertyPath; import org.springframework.data.relational.core.conversion.DbAction.Insert; +import org.springframework.data.relational.core.conversion.DbAction.InsertRoot; +import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; -import org.springframework.data.relational.core.mapping.NamingStrategy; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** * Unit tests for {@link DefaultJdbcInterpreter} @@ -61,8 +59,8 @@ public class DefaultJdbcInterpreterUnitTests { Element element = new Element(); - Insert containerInsert = DbAction.insert(container, RelationalPropertyPath.from("", Container.class), null); - Insert insert = DbAction.insert(element, RelationalPropertyPath.from("element", Container.class), containerInsert); + InsertRoot containerInsert = new InsertRoot<>(container); + Insert insert = new Insert<>(element, PropertyPathUtils.toPath("element", Container.class, context), containerInsert); interpreter.interpret(insert); diff --git a/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java index c2e51fba..58da3f33 100644 --- a/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java @@ -22,14 +22,14 @@ import static org.mockito.Mockito.*; import java.util.Collections; import org.apache.ibatis.session.SqlSession; -import org.apache.ibatis.session.SqlSessionFactory; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.data.jdbc.mybatis.MyBatisContext; import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy; -import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; /** @@ -39,11 +39,20 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProp */ public class MyBatisDataAccessStrategyUnitTests { + RelationalMappingContext context = new RelationalMappingContext(); + SqlSession session = mock(SqlSession.class); ArgumentCaptor captor = ArgumentCaptor.forClass(MyBatisContext.class); MyBatisDataAccessStrategy accessStrategy = new MyBatisDataAccessStrategy(session); + PersistentPropertyPath path(String path, Class source) { + + RelationalMappingContext context = this.context; + return PropertyPathUtils.toPath(path, source, context); + + } + @Before public void before() { @@ -119,9 +128,11 @@ public class MyBatisDataAccessStrategyUnitTests { @Test // DATAJDBC-123 public void deleteAllByPath() { - accessStrategy.deleteAll(PropertyPath.from("class.name.bytes", String.class)); + accessStrategy.deleteAll(path("one.two", DummyEntity.class)); - verify(session).delete(eq("java.lang.StringMapper.deleteAll-class-name-bytes"), captor.capture()); + verify(session).delete( + eq("org.springframework.data.jdbc.core.MyBatisDataAccessStrategyUnitTests$DummyEntityMapper.deleteAll-one-two"), + captor.capture()); assertThat(captor.getValue()) // .isNotNull() // @@ -133,7 +144,7 @@ public class MyBatisDataAccessStrategyUnitTests { ).containsExactly( // null, // null, // - byte[].class, // + ChildTwo.class, // null // ); } @@ -163,9 +174,11 @@ public class MyBatisDataAccessStrategyUnitTests { @Test // DATAJDBC-123 public void deleteByPath() { - accessStrategy.delete("rootid", PropertyPath.from("class.name.bytes", String.class)); + accessStrategy.delete("rootid", path("one.two", DummyEntity.class)); - verify(session).delete(eq("java.lang.StringMapper.delete-class-name-bytes"), captor.capture()); + verify(session).delete( + eq("org.springframework.data.jdbc.core.MyBatisDataAccessStrategyUnitTests$DummyEntityMapper.delete-one-two"), + captor.capture()); assertThat(captor.getValue()) // .isNotNull() // @@ -176,7 +189,7 @@ public class MyBatisDataAccessStrategyUnitTests { c -> c.get("key") // ).containsExactly( // null, "rootid", // - byte[].class, // + ChildTwo.class, // null // ); } @@ -304,7 +317,6 @@ public class MyBatisDataAccessStrategyUnitTests { accessStrategy.count(String.class); - verify(session).selectOne(eq("java.lang.StringMapper.count"), captor.capture()); assertThat(captor.getValue()) // @@ -315,11 +327,20 @@ public class MyBatisDataAccessStrategyUnitTests { MyBatisContext::getDomainType, // c -> c.get("key") // ).containsExactly( // - null, // - null, // - String.class, // - null // + null, // + null, // + String.class, // + null // ); } + private static class DummyEntity { + ChildOne one; + } + + private static class ChildOne { + ChildTwo two; + } + + private static class ChildTwo {} } diff --git a/src/test/java/org/springframework/data/jdbc/core/PropertyPathUtils.java b/src/test/java/org/springframework/data/jdbc/core/PropertyPathUtils.java new file mode 100644 index 00000000..555d30ba --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/PropertyPathUtils.java @@ -0,0 +1,42 @@ +package org.springframework.data.jdbc.core; + +/* + * 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. + * 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. + */ + +import lombok.experimental.UtilityClass; + +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; + +/** + * Utility class for easy creation of {@link PersistentPropertyPath} instances for tests. + * + * @author Jens Schauder + */ +@UtilityClass +class PropertyPathUtils { + + static PersistentPropertyPath toPath(String path, Class source, + RelationalMappingContext context) { + + PersistentPropertyPaths persistentPropertyPaths = context + .findPersistentPropertyPaths(source, p -> true); + + return persistentPropertyPaths.filter(p -> p.toDotPath().equals(path)).stream().findFirst().orElse(null); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java index bf0834ce..9099a8d4 100644 --- a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java @@ -25,10 +25,12 @@ import java.util.function.Consumer; import org.assertj.core.api.SoftAssertions; import org.junit.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.relational.core.mapping.NamingStrategy; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; /** * Unit tests to verify a contextual {@link NamingStrategy} implementation that customizes using a user-centric @@ -39,7 +41,8 @@ import org.springframework.data.relational.core.mapping.NamingStrategy; */ public class SqlGeneratorContextBasedNamingStrategyUnitTests { - private final ThreadLocal userHandler = new ThreadLocal<>(); + RelationalMappingContext context = new RelationalMappingContext(); + ThreadLocal userHandler = new ThreadLocal<>(); /** * Use a {@link NamingStrategy}, but override the schema with a {@link ThreadLocal}-based setting. @@ -80,9 +83,9 @@ public class SqlGeneratorContextBasedNamingStrategyUnitTests { SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); - String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM " + user + ".referenced_entity WHERE " + user + ".dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM " + user + ".referenced_entity WHERE " + "dummy_entity = :rootId"); }); } @@ -93,13 +96,13 @@ public class SqlGeneratorContextBasedNamingStrategyUnitTests { SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); - String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( // "DELETE FROM " + user + ".second_level_referenced_entity " // - + "WHERE " + user + ".referenced_entity IN " // + + "WHERE " + "referenced_entity IN " // + "(SELECT l1id FROM " + user + ".referenced_entity " // - + "WHERE " + user + ".dummy_entity = :rootId)"); + + "WHERE " + "dummy_entity = :rootId)"); }); } @@ -123,10 +126,10 @@ public class SqlGeneratorContextBasedNamingStrategyUnitTests { SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); - String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref", DummyEntity.class)); assertThat(sql).isEqualTo( // - "DELETE FROM " + user + ".referenced_entity WHERE " + user + ".dummy_entity IS NOT NULL"); + "DELETE FROM " + user + ".referenced_entity WHERE " + "dummy_entity IS NOT NULL"); }); } @@ -137,16 +140,20 @@ public class SqlGeneratorContextBasedNamingStrategyUnitTests { SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); - String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( // "DELETE FROM " + user + ".second_level_referenced_entity " // - + "WHERE " + user + ".referenced_entity IN " // + + "WHERE " + "referenced_entity IN " // + "(SELECT l1id FROM " + user + ".referenced_entity " // - + "WHERE " + user + ".dummy_entity IS NOT NULL)"); + + "WHERE " + "dummy_entity IS NOT NULL)"); }); } + private PersistentPropertyPath getPath(String path, Class baseType) { + return PersistentPropertyPathTestUtils.getPath(this.context, path, baseType); + } + /** * Take a set of user-based assertions and run them against multiple users, in different threads. */ diff --git a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java index 5c419403..e25e5582 100644 --- a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -20,6 +20,8 @@ import static org.assertj.core.api.Assertions.*; import org.assertj.core.api.SoftAssertions; import org.junit.Test; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; +import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -65,6 +67,8 @@ public class SqlGeneratorFixedNamingStrategyUnitTests { } }; + private RelationalMappingContext context = new RelationalMappingContext(); + @Test // DATAJDBC-107 public void findOneWithOverriddenFixedTableName() { @@ -108,10 +112,10 @@ public class SqlGeneratorFixedNamingStrategyUnitTests { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); - String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " - + "WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity = :rootId"); + + "WHERE dummy_entity = :rootId"); } @Test // DATAJDBC-107 @@ -119,12 +123,12 @@ public class SqlGeneratorFixedNamingStrategyUnitTests { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); - String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_SecondLevelReferencedEntity " - + "WHERE FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity IN " + + "WHERE referenced_entity IN " + "(SELECT FixedCustomPropertyPrefix_l1id " + "FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " - + "WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity = :rootId)"); + + "WHERE dummy_entity = :rootId)"); } @Test // DATAJDBC-107 @@ -142,10 +146,10 @@ public class SqlGeneratorFixedNamingStrategyUnitTests { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); - String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref", DummyEntity.class)); assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " - + "WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity IS NOT NULL"); + + "WHERE dummy_entity IS NOT NULL"); } @Test // DATAJDBC-107 @@ -153,12 +157,12 @@ public class SqlGeneratorFixedNamingStrategyUnitTests { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); - String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_SecondLevelReferencedEntity " - + "WHERE FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity IN " + + "WHERE referenced_entity IN " + "(SELECT FixedCustomPropertyPrefix_l1id " + "FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " - + "WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity IS NOT NULL)"); + + "WHERE dummy_entity IS NOT NULL)"); } @Test // DATAJDBC-113 @@ -172,6 +176,10 @@ public class SqlGeneratorFixedNamingStrategyUnitTests { "DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_DummyEntity WHERE FixedCustomPropertyPrefix_id IN (:ids)"); } + private PersistentPropertyPath getPath(String path, Class baseType) { + return PersistentPropertyPathTestUtils.getPath(context, path, baseType); + } + /** * Plug in a custom {@link NamingStrategy} for this test case. * diff --git a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java index d605d64d..12e01fe4 100644 --- a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java @@ -24,6 +24,8 @@ import org.assertj.core.api.SoftAssertions; import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; +import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -39,6 +41,7 @@ import org.springframework.data.relational.core.mapping.NamingStrategy; public class SqlGeneratorUnitTests { private SqlGenerator sqlGenerator; + private RelationalMappingContext context = new RelationalMappingContext(); @Before public void setUp() { @@ -69,7 +72,7 @@ public class SqlGeneratorUnitTests { @Test // DATAJDBC-112 public void cascadingDeleteFirstLevel() { - String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE dummy_entity = :rootId"); } @@ -77,7 +80,7 @@ public class SqlGeneratorUnitTests { @Test // DATAJDBC-112 public void cascadingDeleteAllSecondLevel() { - String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( "DELETE FROM second_level_referenced_entity WHERE referenced_entity IN (SELECT x_l1id FROM referenced_entity WHERE dummy_entity = :rootId)"); @@ -94,7 +97,7 @@ public class SqlGeneratorUnitTests { @Test // DATAJDBC-112 public void cascadingDeleteAllFirstLevel() { - String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref", DummyEntity.class)); assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE dummy_entity IS NOT NULL"); } @@ -102,12 +105,28 @@ public class SqlGeneratorUnitTests { @Test // DATAJDBC-112 public void cascadingDeleteSecondLevel() { - String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( "DELETE FROM second_level_referenced_entity WHERE referenced_entity IN (SELECT x_l1id FROM referenced_entity WHERE dummy_entity IS NOT NULL)"); } + @Test // DATAJDBC-227 + public void deleteAllMap() { + + String sql = sqlGenerator.createDeleteAllSql(getPath("mappedElements", DummyEntity.class)); + + assertThat(sql).isEqualTo("DELETE FROM element WHERE dummy_entity IS NOT NULL"); + } + + @Test // DATAJDBC-227 + public void deleteMapByPath() { + + String sql = sqlGenerator.createDeleteByPath(getPath("mappedElements", DummyEntity.class)); + + assertThat(sql).isEqualTo("DELETE FROM element WHERE dummy_entity = :rootId"); + } + @Test // DATAJDBC-131 public void findAllByProperty() { @@ -151,6 +170,10 @@ public class SqlGeneratorUnitTests { + "WHERE back-ref = :back-ref " + "ORDER BY key-column"); } + + private PersistentPropertyPath getPath(String path, Class base) { + return PersistentPropertyPathTestUtils.getPath(context, path, base); + } @SuppressWarnings("unused") static class DummyEntity { diff --git a/src/test/java/org/springframework/data/jdbc/core/mapping/PersistentPropertyPathTestUtils.java b/src/test/java/org/springframework/data/jdbc/core/mapping/PersistentPropertyPathTestUtils.java new file mode 100644 index 00000000..973aae1d --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/mapping/PersistentPropertyPathTestUtils.java @@ -0,0 +1,41 @@ +/* + * 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. + * 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.jdbc.core.mapping; + +import lombok.experimental.UtilityClass; + +import org.jetbrains.annotations.NotNull; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; + +/** + * @author Jens Schauder + */ +@UtilityClass +public class PersistentPropertyPathTestUtils { + + @NotNull + public static PersistentPropertyPath getPath(RelationalMappingContext context, + String path, Class baseType) { + + return context.findPersistentPropertyPaths(baseType, p -> p.isEntity()) // + .filter(p -> p.toDotPath().equals(path)) // + .stream() // + .findFirst() // + .orElseThrow(() -> new IllegalArgumentException(String.format("No path for %s based on %s", path, baseType))); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContextUnitTests.java b/src/test/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContextUnitTests.java deleted file mode 100644 index 776149b4..00000000 --- a/src/test/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContextUnitTests.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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. - * 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.jdbc.mapping.model; - -import static org.assertj.core.api.Assertions.*; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.junit.Test; -import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; - -/** - * @author Jens Schauder - */ -public class JdbcMappingContextUnitTests { - - RelationalMappingContext context = new RelationalMappingContext(); - - // DATAJDBC-188 - @Test - public void simpleEntityDoesntReferenceOtherEntities() { - - List paths = context.referencedEntities(SimpleEntity.class, null); - - assertThat(paths).isEmpty(); - } - - // DATAJDBC-188 - @Test - public void cascadingReferencesGetFound() { - - List paths = context.referencedEntities(CascadingEntity.class, null); - - assertThat(paths).extracting(PropertyPath::toDotPath) // - .containsExactly( // - "reference.reference", // - "reference" // - ); - } - - // DATAJDBC-188 - @Test - public void setReferencesGetFound() { - - List paths = context.referencedEntities(EntityWithSet.class, null); - - assertThat(paths).extracting(PropertyPath::toDotPath) // - .containsExactly( // - "set.reference", // - "set" // - ); - } - - // DATAJDBC-188 - @Test - public void mapReferencesGetFound() { - - List paths = context.referencedEntities(EntityWithMap.class, null); - - assertThat(paths).extracting(PropertyPath::toDotPath) // - .containsExactly( // - "map.reference", // - "map" // - ); - } - - private static class SimpleEntity { - String name; - } - - private static class CascadingEntity { - MiddleEntity reference; - } - - private static class MiddleEntity { - SimpleEntity reference; - } - - private static class EntityWithMap { - Map map; - } - - private static class EntityWithSet { - Set set; - } -} diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java index 9eca36b8..eb9cd07c 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java @@ -80,7 +80,7 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests { entity.id, // entity.name, // true) // - ); + ); } @@ -103,14 +103,14 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests { one.id, // one.name, // true) // - ); + ); assertThat(repository.findById(two.id)) // .contains(new DummyEntity( // two.id, // two.name, // true) // - ); + ); } @Test // DATAJDBC-120 @@ -205,7 +205,7 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests { List> actions = event.getChange().getActions(); actions.clear(); - actions.add(DbAction.update(entity, null, null)); + actions.add(new DbAction.UpdateRoot<>(entity)); }; } @@ -223,7 +223,7 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests { log.text = entity.name + " saved"; List> actions = event.getChange().getActions(); - actions.add(DbAction.insert(log, null, null)); + actions.add(new DbAction.InsertRoot<>(log)); }; } } diff --git a/src/test/java/org/springframework/data/relational/core/conversion/DbActionUnitTests.java b/src/test/java/org/springframework/data/relational/core/conversion/DbActionUnitTests.java index 3971a826..b0984c6e 100644 --- a/src/test/java/org/springframework/data/relational/core/conversion/DbActionUnitTests.java +++ b/src/test/java/org/springframework/data/relational/core/conversion/DbActionUnitTests.java @@ -15,16 +15,12 @@ */ package org.springframework.data.relational.core.conversion; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.*; import org.junit.Test; -import org.springframework.data.relational.core.conversion.DbAction; -import org.springframework.data.relational.core.conversion.DbActionExecutionException; -import org.springframework.data.relational.core.conversion.Interpreter; -import org.springframework.data.relational.core.conversion.RelationalPropertyPath; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** * Unit tests for {@link DbAction}s @@ -33,20 +29,21 @@ import org.springframework.data.relational.core.conversion.RelationalPropertyPat */ public class DbActionUnitTests { + RelationalMappingContext context = new RelationalMappingContext(); + @Test // DATAJDBC-150 public void exceptionFromActionContainsUsefulInformationWhenInterpreterFails() { DummyEntity entity = new DummyEntity(); - DbAction.Insert insert = DbAction.insert(entity, RelationalPropertyPath.from("someName", DummyEntity.class), - null); + DbAction.InsertRoot insert = new DbAction.InsertRoot<>(entity); Interpreter failingInterpreter = mock(Interpreter.class); - doThrow(new RuntimeException()).when(failingInterpreter).interpret(any(DbAction.Insert.class)); + doThrow(new RuntimeException()).when(failingInterpreter).interpret(any(DbAction.InsertRoot.class)); assertThatExceptionOfType(DbActionExecutionException.class) // - .isThrownBy(() -> insert.executeWith(failingInterpreter)) // + .isThrownBy(() -> insert.executeWith(failingInterpreter)) // .withMessageContaining("Insert") // - .withMessageContaining(entity.toString()); + .withMessageContaining(entity.toString()); } diff --git a/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java b/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java index 4eff05e2..c992af9b 100644 --- a/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java +++ b/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java @@ -23,17 +23,16 @@ 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; -import org.springframework.data.relational.core.conversion.DbAction; -import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter; -import org.springframework.data.relational.core.conversion.RelationalPropertyPath; -import org.springframework.data.relational.core.conversion.AggregateChange.Kind; +import org.springframework.data.mapping.PersistentPropertyPath; 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.AggregateChange.Kind; import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** - * Unit tests for the {@link RelationalEntityDeleteWriter} + * Unit tests for the {@link org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter} * * @author Jens Schauder */ @@ -43,9 +42,12 @@ public class RelationalEntityDeleteWriterUnitTests { RelationalEntityDeleteWriter converter = new RelationalEntityDeleteWriter(new RelationalMappingContext()); private static Object dotPath(DbAction dba) { - - RelationalPropertyPath propertyPath = dba.getPropertyPath(); - return propertyPath == null ? null : propertyPath.toDotPath(); + if (dba instanceof DbAction.WithPropertyPath) { + PersistentPropertyPath propertyPath = ((DbAction.WithPropertyPath) dba).getPropertyPath(); + return propertyPath == null ? null : propertyPath.toDotPath(); + } else { + return null; + } } @Test // DATAJDBC-112 @@ -62,16 +64,14 @@ public class RelationalEntityDeleteWriterUnitTests { .containsExactly( // Tuple.tuple(Delete.class, YetAnother.class, "other.yetAnother"), // Tuple.tuple(Delete.class, OtherEntity.class, "other"), // - Tuple.tuple(Delete.class, SomeEntity.class, null) // + Tuple.tuple(DeleteRoot.class, SomeEntity.class, null) // ); } @Test // DATAJDBC-188 public void deleteAllDeletesAllEntitiesAndReferencedEntities() { - SomeEntity entity = new SomeEntity(23L); - - AggregateChange aggregateChange = new AggregateChange(Kind.DELETE, SomeEntity.class, null); + AggregateChange aggregateChange = new AggregateChange<>(Kind.DELETE, SomeEntity.class, null); converter.write(null, aggregateChange); @@ -80,7 +80,7 @@ public class RelationalEntityDeleteWriterUnitTests { .containsExactly( // Tuple.tuple(DeleteAll.class, YetAnother.class, "other.yetAnother"), // Tuple.tuple(DeleteAll.class, OtherEntity.class, "other"), // - Tuple.tuple(DeleteAll.class, SomeEntity.class, null) // + Tuple.tuple(DeleteAllRoot.class, SomeEntity.class, null) // ); } diff --git a/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java b/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java index f3b74bd0..d025596c 100644 --- a/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java +++ b/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java @@ -30,14 +30,11 @@ 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; -import org.springframework.data.relational.core.conversion.DbAction; -import org.springframework.data.relational.core.conversion.RelationalEntityWriter; -import org.springframework.data.relational.core.conversion.AggregateChange.Kind; 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.Insert; -import org.springframework.data.relational.core.conversion.DbAction.Update; +import org.springframework.data.relational.core.conversion.DbAction.InsertRoot; +import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot; +import org.springframework.data.relational.core.conversion.AggregateChange.Kind; import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** @@ -61,31 +58,54 @@ public class RelationalEntityWriterUnitTests { converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) // + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // .containsExactly( // - tuple(Insert.class, SingleReferenceEntity.class, "") // - ); + tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) // + ); } @Test // DATAJDBC-112 - public void existingEntityGetsConvertedToUpdate() { + public void newEntityWithReferenceGetsConvertedToTwoInserts() { + + SingleReferenceEntity entity = new SingleReferenceEntity(null); + entity.other = new Element(null); - SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); AggregateChange aggregateChange = // new AggregateChange(Kind.SAVE, SingleReferenceEntity.class, entity); converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) // + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // .containsExactly( // - tuple(Delete.class, Element.class, "other"), // - tuple(Update.class, SingleReferenceEntity.class, "") // - ); + tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false), // + tuple(Insert.class, Element.class, "other", Element.class, true) // + ); } @Test // DATAJDBC-112 - public void referenceTriggersDeletePlusInsert() { + public void existingEntityGetsConvertedToDeletePlusUpdate() { + + SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); + + AggregateChange aggregateChange = // + new AggregateChange(Kind.SAVE, SingleReferenceEntity.class, entity); + + converter.write(entity, aggregateChange); + + assertThat(aggregateChange.getActions()) // + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // + .containsExactly( // + tuple(Delete.class, Element.class, "other", null, false), // + tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) // + ); + } + + @Test // DATAJDBC-112 + public void newReferenceTriggersDeletePlusInsert() { SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); entity.other = new Element(null); @@ -96,26 +116,29 @@ public class RelationalEntityWriterUnitTests { converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) // + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // .containsExactly( // - tuple(Delete.class, Element.class, "other"), // - tuple(Update.class, SingleReferenceEntity.class, ""), // - tuple(Insert.class, Element.class, "other") // - ); + tuple(Delete.class, Element.class, "other", null, false), // + tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false), // + tuple(Insert.class, Element.class, "other", Element.class, true) // + ); } @Test // DATAJDBC-113 public void newEntityWithEmptySetResultsInSingleInsert() { SetContainer entity = new SetContainer(null); - AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, SetContainer.class, entity); + AggregateChange aggregateChange = new AggregateChange( + Kind.SAVE, SetContainer.class, entity); converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) // + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // .containsExactly( // - tuple(Insert.class, SetContainer.class, "")); + tuple(InsertRoot.class, SetContainer.class, "", SetContainer.class, false)); } @Test // DATAJDBC-113 @@ -128,12 +151,14 @@ public class RelationalEntityWriterUnitTests { AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, SetContainer.class, entity); converter.write(entity, aggregateChange); - assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) // + assertThat(aggregateChange.getActions()) + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // .containsExactly( // - tuple(Insert.class, SetContainer.class, ""), // - tuple(Insert.class, Element.class, "elements"), // - tuple(Insert.class, Element.class, "elements") // - ); + tuple(InsertRoot.class, SetContainer.class, "", SetContainer.class, false), // + tuple(Insert.class, Element.class, "elements", Element.class, true), // + tuple(Insert.class, Element.class, "elements", Element.class, true) // + ); } @Test // DATAJDBC-113 @@ -151,20 +176,25 @@ public class RelationalEntityWriterUnitTests { new Element(null)) // ); - AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, SetContainer.class, entity); + AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, + CascadingReferenceEntity.class, entity); converter.write(entity, aggregateChange); - assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) // + assertThat(aggregateChange.getActions()) + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // .containsExactly( // - tuple(Insert.class, CascadingReferenceEntity.class, ""), // - tuple(Insert.class, CascadingReferenceMiddleElement.class, "other"), // - tuple(Insert.class, Element.class, "other.element"), // - tuple(Insert.class, Element.class, "other.element"), // - tuple(Insert.class, CascadingReferenceMiddleElement.class, "other"), // - tuple(Insert.class, Element.class, "other.element"), // - tuple(Insert.class, Element.class, "other.element") // - ); + tuple(InsertRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false), // + tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, + true), // + tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, + true), // + tuple(Insert.class, Element.class, "other.element", Element.class, true), // + tuple(Insert.class, Element.class, "other.element", Element.class, true), // + tuple(Insert.class, Element.class, "other.element", Element.class, true), // + tuple(Insert.class, Element.class, "other.element", Element.class, true) // + ); } @Test // DATAJDBC-188 @@ -182,21 +212,26 @@ public class RelationalEntityWriterUnitTests { new Element(null)) // ); - AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, CascadingReferenceEntity.class, entity); + AggregateChange aggregateChange = new AggregateChange(Kind.SAVE, + CascadingReferenceEntity.class, entity); converter.write(entity, aggregateChange); - assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) // + assertThat(aggregateChange.getActions()) + .extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath, this::actualEntityType, + this::isWithDependsOn) // .containsExactly( // - tuple(Delete.class, Element.class, "other.element"), - tuple(Delete.class, CascadingReferenceMiddleElement.class, "other"), - tuple(Update.class, CascadingReferenceEntity.class, ""), // - tuple(Insert.class, CascadingReferenceMiddleElement.class, "other"), // - tuple(Insert.class, Element.class, "other.element"), // - tuple(Insert.class, Element.class, "other.element"), // - tuple(Insert.class, CascadingReferenceMiddleElement.class, "other"), // - tuple(Insert.class, Element.class, "other.element"), // - tuple(Insert.class, Element.class, "other.element") // + tuple(Delete.class, Element.class, "other.element", null, false), + tuple(Delete.class, CascadingReferenceMiddleElement.class, "other", null, false), + tuple(UpdateRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false), // + tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, + true), // + tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, + true), // + tuple(Insert.class, Element.class, "other.element", Element.class, true), // + tuple(Insert.class, Element.class, "other.element", Element.class, true), // + tuple(Insert.class, Element.class, "other.element", Element.class, true), // + tuple(Insert.class, Element.class, "other.element", Element.class, true) // ); } @@ -210,7 +245,7 @@ public class RelationalEntityWriterUnitTests { assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) // .containsExactly( // - tuple(Insert.class, MapContainer.class, "")); + tuple(InsertRoot.class, MapContainer.class, "")); } @Test // DATAJDBC-131 @@ -226,16 +261,16 @@ public class RelationalEntityWriterUnitTests { assertThat(aggregateChange.getActions()) .extracting(DbAction::getClass, DbAction::getEntityType, this::getMapKey, this::extractPath) // .containsExactlyInAnyOrder( // - tuple(Insert.class, MapContainer.class, null, ""), // + tuple(InsertRoot.class, MapContainer.class, null, ""), // tuple(Insert.class, Element.class, "one", "elements"), // tuple(Insert.class, Element.class, "two", "elements") // ).containsSubsequence( // container comes before the elements - tuple(Insert.class, MapContainer.class, null, ""), // + tuple(InsertRoot.class, MapContainer.class, null, ""), // tuple(Insert.class, Element.class, "two", "elements") // ).containsSubsequence( // container comes before the elements - tuple(Insert.class, MapContainer.class, null, ""), // + tuple(InsertRoot.class, MapContainer.class, null, ""), // tuple(Insert.class, Element.class, "one", "elements") // - ); + ); } @Test // DATAJDBC-183 @@ -261,7 +296,7 @@ public class RelationalEntityWriterUnitTests { assertThat(aggregateChange.getActions()) .extracting(DbAction::getClass, DbAction::getEntityType, this::getMapKey, this::extractPath) // .containsExactlyInAnyOrder( // - tuple(Insert.class, MapContainer.class, null, ""), // + tuple(InsertRoot.class, MapContainer.class, null, ""), // tuple(Insert.class, Element.class, "1", "elements"), // tuple(Insert.class, Element.class, "2", "elements"), // tuple(Insert.class, Element.class, "3", "elements"), // @@ -287,7 +322,7 @@ public class RelationalEntityWriterUnitTests { assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) // .containsExactly( // - tuple(Insert.class, ListContainer.class, "")); + tuple(InsertRoot.class, ListContainer.class, "")); } @Test // DATAJDBC-130 @@ -303,16 +338,16 @@ public class RelationalEntityWriterUnitTests { assertThat(aggregateChange.getActions()) .extracting(DbAction::getClass, DbAction::getEntityType, this::getListKey, this::extractPath) // .containsExactlyInAnyOrder( // - tuple(Insert.class, ListContainer.class, null, ""), // + tuple(InsertRoot.class, ListContainer.class, null, ""), // tuple(Insert.class, Element.class, 0, "elements"), // tuple(Insert.class, Element.class, 1, "elements") // ).containsSubsequence( // container comes before the elements - tuple(Insert.class, ListContainer.class, null, ""), // + tuple(InsertRoot.class, ListContainer.class, null, ""), // tuple(Insert.class, Element.class, 1, "elements") // ).containsSubsequence( // container comes before the elements - tuple(Insert.class, ListContainer.class, null, ""), // + tuple(InsertRoot.class, ListContainer.class, null, ""), // tuple(Insert.class, Element.class, 0, "elements") // - ); + ); } @Test // DATAJDBC-131 @@ -329,7 +364,7 @@ public class RelationalEntityWriterUnitTests { .extracting(DbAction::getClass, DbAction::getEntityType, this::getMapKey, this::extractPath) // .containsExactly( // tuple(Delete.class, Element.class, null, "elements"), // - tuple(Update.class, MapContainer.class, null, ""), // + tuple(UpdateRoot.class, MapContainer.class, null, ""), // tuple(Insert.class, Element.class, "one", "elements") // ); } @@ -348,7 +383,7 @@ public class RelationalEntityWriterUnitTests { .extracting(DbAction::getClass, DbAction::getEntityType, this::getListKey, this::extractPath) // .containsExactly( // tuple(Delete.class, Element.class, null, "elements"), // - tuple(Update.class, ListContainer.class, null, ""), // + tuple(UpdateRoot.class, ListContainer.class, null, ""), // tuple(Insert.class, Element.class, 0, "elements") // ); } @@ -362,15 +397,32 @@ public class RelationalEntityWriterUnitTests { } private Object getMapKey(DbAction a) { - return a.getAdditionalValues().get("map_container_key"); + return a instanceof DbAction.WithDependingOn ? ((DbAction.WithDependingOn) a).getAdditionalValues().get("map_container_key") : null; } private Object getListKey(DbAction a) { - return a.getAdditionalValues().get("list_container_key"); + return a instanceof DbAction.WithDependingOn ? ((DbAction.WithDependingOn) a).getAdditionalValues().get("list_container_key") : null; } private String extractPath(DbAction action) { - return action.getPropertyPath().toDotPath(); + + 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 @@ -382,6 +434,15 @@ public class RelationalEntityWriterUnitTests { String name; } + @RequiredArgsConstructor + static class ReferenceWoIdEntity { + + @Id final Long id; + NoIdElement other; + // should not trigger own Dbaction + String name; + } + @RequiredArgsConstructor private static class CascadingReferenceMiddleElement { @@ -422,4 +483,10 @@ public class RelationalEntityWriterUnitTests { @Id final Long id; } + @RequiredArgsConstructor + private static class NoIdElement { + // empty classes feel weird. + String name; + } + } diff --git a/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java b/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java deleted file mode 100644 index 34f1ec00..00000000 --- a/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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. - * 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.mapping; - -import static org.assertj.core.api.Assertions.*; - -import java.util.List; - -import org.junit.Test; -import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; - -/** - * Unit tests for {@link RelationalMappingContext}. - * - * @author Jens Schauder - * @author Oliver Gierke - */ -public class RelationalMappingContextUnitTests { - - @Test // DATAJDBC-142 - public void referencedEntitiesGetFound() { - - RelationalMappingContext mappingContext = new RelationalMappingContext(); - - List propertyPaths = mappingContext.referencedEntities(DummyEntity.class, null); - - assertThat(propertyPaths) // - .extracting(PropertyPath::toDotPath) // - .containsExactly("one.two", "one"); - } - - @Test // DATAJDBC-142 - public void propertyPathDoesNotDependOnNamingStrategy() { - - RelationalMappingContext mappingContext = new RelationalMappingContext(); - - List propertyPaths = mappingContext.referencedEntities(DummyEntity.class, null); - - assertThat(propertyPaths) // - .extracting(PropertyPath::toDotPath) // - .containsExactly("one.two", "one"); - } - - static class DummyEntity { - - String simpleProperty; - LevelOne one; - } - - static class LevelOne { - LevelTwo two; - } - - static class LevelTwo { - String someValue; - } -} diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-mariadb.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-mariadb.sql new file mode 100644 index 00000000..119c6082 --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-mariadb.sql @@ -0,0 +1,2 @@ +CREATE TABLE dummy_entity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE element (content VARCHAR(100), dummy_entity BIGINT); diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-mysql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-mysql.sql new file mode 100644 index 00000000..119c6082 --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-mysql.sql @@ -0,0 +1,2 @@ +CREATE TABLE dummy_entity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE element (content VARCHAR(100), dummy_entity BIGINT); diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-postgres.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-postgres.sql new file mode 100644 index 00000000..8c9eb5b4 --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsNoIdIntegrationTests-postgres.sql @@ -0,0 +1,4 @@ +DROP TABLE element; +DROP TABLE dummy_entity; +CREATE TABLE dummy_entity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE element (content VARCHAR(100), dummy_entity BIGINT);