diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java index 9d0c3e46..50814172 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -28,6 +28,6 @@ public class EntityRowMapper extends org.springframework.data.jdbc.core.conve public EntityRowMapper(RelationalPersistentEntity entity, JdbcConverter converter, DataAccessStrategy accessStrategy) { - super(entity, converter, accessStrategy); + super(entity, converter); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilder.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilder.java index 87dc0406..3616cc69 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilder.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilder.java @@ -15,9 +15,7 @@ */ package org.springframework.data.jdbc.core; -import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.Identifier; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -54,18 +52,6 @@ public class JdbcIdentifierBuilder { return new JdbcIdentifierBuilder(identifier); } - private static RelationalPersistentProperty getLastIdProperty( - PersistentPropertyPath path) { - - RelationalPersistentProperty idProperty = path.getRequiredLeafProperty().getOwner().getIdProperty(); - - if (idProperty != null) { - return idProperty; - } - - return getLastIdProperty(path.getParentPath()); - } - /** * Adds a qualifier to the identifier to build. A qualifier is a map key or a list index. * @@ -78,7 +64,7 @@ public class JdbcIdentifierBuilder { Assert.notNull(path, "Path must not be null"); Assert.notNull(value, "Value must not be null"); - identifier = identifier.withPart(path.getKeyColumn(), value, path.getQualifierColumnType()); + identifier = identifier.withPart(path.getQualifierColumn(), value, path.getQualifierColumnType()); return this; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java index 70289e00..b41a8561 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java @@ -31,6 +31,7 @@ import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.SimpleTypeHolder; @@ -39,6 +40,7 @@ import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -53,6 +55,7 @@ import org.springframework.util.Assert; * @author Mark Paluch * @author Jens Schauder * @author Christoph Strobl + * @since 1.1 * @see MappingContext * @see SimpleTypeHolder * @see CustomConversions @@ -61,70 +64,51 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc private static final Logger LOG = LoggerFactory.getLogger(BasicJdbcConverter.class); private static final Converter, Map> ITERABLE_OF_ENTRY_TO_MAP_CONVERTER = new IterableOfEntryToMapConverter(); + private final JdbcTypeFactory typeFactory; + private RelationResolver relationResolver; + /** * Creates a new {@link BasicRelationalConverter} given {@link MappingContext} and a * {@link JdbcTypeFactory#unsupported() no-op type factory} throwing {@link UnsupportedOperationException} on type - * creation. Use {@link #BasicJdbcConverter(MappingContext, JdbcTypeFactory)} to convert arrays and large objects into - * JDBC-specific types. + * creation. Use {@link #BasicJdbcConverter(MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and + * large objects into JDBC-specific types. * * @param context must not be {@literal null}. + * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. */ public BasicJdbcConverter( - MappingContext, ? extends RelationalPersistentProperty> context) { - this(context, JdbcTypeFactory.unsupported()); + MappingContext, ? extends RelationalPersistentProperty> context, + RelationResolver relationResolver) { + + super(context); + + Assert.notNull(relationResolver, "RelationResolver must not be null"); + + this.relationResolver = relationResolver; + this.typeFactory = JdbcTypeFactory.unsupported(); } /** * Creates a new {@link BasicRelationalConverter} given {@link MappingContext}. * * @param context must not be {@literal null}. + * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. * @param typeFactory must not be {@literal null} * @since 1.1 */ public BasicJdbcConverter( MappingContext, ? extends RelationalPersistentProperty> context, - JdbcTypeFactory typeFactory) { - - super(context); - - Assert.notNull(typeFactory, "JdbcTypeFactory must not be null"); - - this.typeFactory = typeFactory; - } - - /** - * Creates a new {@link BasicRelationalConverter} given {@link MappingContext}, {@link CustomConversions}, and - * {@link JdbcTypeFactory}. - * - * @param context must not be {@literal null}. - * @param conversions must not be {@literal null}. - * @param typeFactory must not be {@literal null} - * @since 1.1 - */ - public BasicJdbcConverter( - MappingContext, ? extends RelationalPersistentProperty> context, - CustomConversions conversions, JdbcTypeFactory typeFactory) { + RelationResolver relationResolver, CustomConversions conversions, JdbcTypeFactory typeFactory) { super(context, conversions); Assert.notNull(typeFactory, "JdbcTypeFactory must not be null"); - this.typeFactory = typeFactory; - } + Assert.notNull(relationResolver, "RelationResolver must not be null"); - /** - * Creates a new {@link BasicRelationalConverter} given {@link MappingContext} and {@link CustomConversions}. - * - * @param context must not be {@literal null}. - * @param conversions must not be {@literal null}. - * @deprecated use one of the constructors with {@link JdbcTypeFactory} parameter. - */ - @Deprecated - public BasicJdbcConverter( - MappingContext, ? extends RelationalPersistentProperty> context, - CustomConversions conversions) { - this(context, conversions, JdbcTypeFactory.unsupported()); + this.relationResolver = relationResolver; + this.typeFactory = typeFactory; } /* @@ -145,9 +129,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc if (AggregateReference.class.isAssignableFrom(type.getType())) { - TypeInformation idType = type.getSuperTypeInformation(AggregateReference.class).getTypeArguments().get(1); - - return AggregateReference.to(readValue(value, idType)); + return readAggregateReference(value, type); } if (value instanceof Array) { @@ -161,6 +143,14 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc return super.readValue(value, type); } + @SuppressWarnings("ConstantConditions") + private Object readAggregateReference(@Nullable Object value, TypeInformation type) { + + TypeInformation idType = type.getSuperTypeInformation(AggregateReference.class).getTypeArguments().get(1); + + return AggregateReference.to(readValue(value, idType)); + } + /* * (non-Javadoc) * @see org.springframework.data.relational.core.conversion.RelationalConverter#writeValue(java.lang.Object, org.springframework.data.util.TypeInformation) @@ -246,47 +236,63 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc return null; } - /* - * (non-Javadoc) - * @see org.springframework.jdbc.core.RowMapper#mapRow(java.sql.ResultSet, int) - */ + @SuppressWarnings("unchecked") @Override - public T mapRow(RelationalPersistentEntity entity, DataAccessStrategy accessStrategy, ResultSet resultSet) { - return new ReadingContext(entity, accessStrategy, resultSet).mapRow(); + public T mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key) { + return new ReadingContext( + new PersistentPropertyPathExtension( + (MappingContext, RelationalPersistentProperty>) getMappingContext(), entity), + resultSet, Identifier.empty(), key).mapRow(); + } + + @Override + public T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key) { + return new ReadingContext(path, resultSet, identifier, key).mapRow(); } private class ReadingContext { private final RelationalPersistentEntity entity; - private final RelationalPersistentProperty idProperty; private final ResultSet resultSet; - PersistentPropertyPathExtension path; - private final DataAccessStrategy accessStrategy; + private final PersistentPropertyPathExtension rootPath; + private final PersistentPropertyPathExtension path; + private final Identifier identifier; + private final Object key; - ReadingContext(RelationalPersistentEntity entity, DataAccessStrategy accessStrategy, ResultSet resultSet) { + @SuppressWarnings("unchecked") + private ReadingContext(PersistentPropertyPathExtension rootPath, ResultSet resultSet, Identifier identifier, + Object key) { + + + RelationalPersistentEntity entity = (RelationalPersistentEntity) rootPath.getLeafEntity(); + + Assert.notNull(entity, "The rootPath must point to an entity."); this.entity = entity; - this.idProperty = entity.getIdProperty(); - this.accessStrategy = accessStrategy; this.resultSet = resultSet; + this.rootPath = rootPath; this.path = new PersistentPropertyPathExtension( - (MappingContext, RelationalPersistentProperty>) getMappingContext(), entity); + (MappingContext, RelationalPersistentProperty>) getMappingContext(), this.entity); + this.identifier = identifier; + this.key = key; } - public ReadingContext(RelationalPersistentEntity entity, DataAccessStrategy accessStrategy, ResultSet resultSet, - PersistentPropertyPathExtension path) { + private ReadingContext(RelationalPersistentEntity entity, ResultSet resultSet, + PersistentPropertyPathExtension rootPath, PersistentPropertyPathExtension path, Identifier identifier, + Object key) { this.entity = entity; - this.idProperty = entity.getIdProperty(); - this.accessStrategy = accessStrategy; this.resultSet = resultSet; + this.rootPath = rootPath; this.path = path; + this.identifier = identifier; + this.key = key; } - private ReadingContext extendBy(RelationalPersistentProperty property) { - return new ReadingContext(getMappingContext().getRequiredPersistentEntity(property.getActualType()), - accessStrategy, resultSet, path.extendBy(property)); + private ReadingContext extendBy(RelationalPersistentProperty property) { + return new ReadingContext((RelationalPersistentEntity) getMappingContext().getRequiredPersistentEntity(property.getActualType()), resultSet, + rootPath.extendBy(property), path.extendBy(property), identifier, key); } T mapRow() { @@ -319,10 +325,14 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc @Nullable private Object readOrLoadProperty(@Nullable Object id, RelationalPersistentProperty property) { - if (property.isCollectionLike() && property.isEntity() && id != null) { - return accessStrategy.findAllByProperty(id, property); - } else if (property.isMap() && id != null) { - return ITERABLE_OF_ENTRY_TO_MAP_CONVERTER.convert(accessStrategy.findAllByProperty(id, property)); + if ((property.isCollectionLike() && property.isEntity()) || property.isMap()) { + + Iterable allByPath = resolveRelation(id, property); + + return property.isMap() // + ? ITERABLE_OF_ENTRY_TO_MAP_CONVERTER.convert(allByPath) // + : allByPath; + } else if (property.isEmbedded()) { return readEmbeddedEntityFrom(id, property); } else { @@ -330,6 +340,18 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc } } + private Iterable resolveRelation(@Nullable Object id, RelationalPersistentProperty property) { + + Identifier identifier = id == null // + ? this.identifier.withPart(rootPath.getQualifierColumn(), key, Object.class) // + : Identifier.of(rootPath.extendBy(property).getReverseColumnName(), id, Object.class); + + PersistentPropertyPath propertyPath = path.extendBy(property) + .getRequiredPersistentPropertyPath(); + + return relationResolver.findAllByPath(identifier, propertyPath); + } + /** * Read a single value or a complete Entity from the {@link ResultSet} passed as an argument. * @@ -355,10 +377,12 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc return newContext.hasInstanceValues(idValue) ? newContext.createInstanceInternal(idValue) : null; } - private boolean hasInstanceValues(Object idValue) { + private boolean hasInstanceValues(@Nullable Object idValue) { RelationalPersistentEntity persistentEntity = path.getLeafEntity(); + Assert.state(persistentEntity != null, "Entity must not be null"); + for (RelationalPersistentProperty embeddedProperty : persistentEntity) { // if the embedded contains Lists, Sets or Maps we consider it non-empty @@ -375,11 +399,11 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc } @Nullable - private S readEntityFrom(RelationalPersistentProperty property, PersistentPropertyPathExtension path) { + private Object readEntityFrom(RelationalPersistentProperty property, PersistentPropertyPathExtension path) { - ReadingContext newContext = (ReadingContext) extendBy(property); + ReadingContext newContext = extendBy(property); - RelationalPersistentEntity entity = (RelationalPersistentEntity) getMappingContext() + RelationalPersistentEntity entity = getMappingContext() .getRequiredPersistentEntity(property.getActualType()); RelationalPersistentProperty idProperty = entity.getIdProperty(); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java index 448377aa..de238003 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java @@ -140,6 +140,15 @@ public class CascadingDataAccessStrategy implements DataAccessStrategy { return collect(das -> das.findAllById(ids, domainType)); } + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.RelationResolver#findAllByPath(org.springframework.data.relational.domain.Identifier, org.springframework.data.mapping.PersistentPropertyPath) + */ + @Override + public Iterable findAllByPath(Identifier identifier, PersistentPropertyPath path) { + return collect(das -> das.findAllByPath(identifier, path)); + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllByProperty(java.lang.Object, org.springframework.data.relational.core.mapping.RelationalPersistentProperty) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java index b42b7de2..ea94e50c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java @@ -30,7 +30,7 @@ import org.springframework.lang.Nullable; * * @author Jens Schauder */ -public interface DataAccessStrategy { +public interface DataAccessStrategy extends RelationResolver { /** * Inserts a the data of a single entity. Referenced entities don't get handled. @@ -144,12 +144,26 @@ public interface DataAccessStrategy { */ Iterable findAllById(Iterable ids, Class domainType); + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.RelationResolver#findAllByPath(org.springframework.data.relational.domain.Identifier, org.springframework.data.mapping.PersistentPropertyPath) + */ + @Override + default Iterable findAllByPath(Identifier identifier, + PersistentPropertyPath path) { + + Object rootId = identifier.toMap().get(path.getRequiredLeafProperty().getReverseColumnName()); + return findAllByProperty(rootId, path.getRequiredLeafProperty()); + }; + /** * Finds all entities reachable via {@literal property} from the instance identified by {@literal rootId}. * * @param rootId Id of the root object on which the {@literal propertyPath} is based. * @param property Leading from the root object to the entities to be found. + * @deprecated Use #findAllByPath instead. */ + @Deprecated Iterable findAllByProperty(Object rootId, RelationalPersistentProperty property); /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index 4b66d647..990f106b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -34,6 +34,7 @@ import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -63,21 +64,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { private final @NonNull RelationalMappingContext context; private final @NonNull JdbcConverter converter; private final @NonNull NamedParameterJdbcOperations operations; - private final @NonNull DataAccessStrategy accessStrategy; - - /** - * Creates a {@link DefaultDataAccessStrategy} which references it self for resolution of recursive data accesses. - * Only suitable if this is the only access strategy in use. - * - * @param sqlGeneratorSource must not be {@literal null}. - * @param context must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param operations must not be {@literal null}. - */ - public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, RelationalMappingContext context, - JdbcConverter converter, NamedParameterJdbcOperations operations) { - this(sqlGeneratorSource, context, converter, operations, null); - } /** * Creates a {@link DefaultDataAccessStrategy} @@ -86,12 +72,10 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { * @param context must not be {@literal null}. * @param converter must not be {@literal null}. * @param operations must not be {@literal null}. - * @param mappingAccessStrategy can be {@literal null}. * @since 1.1 */ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, RelationalMappingContext context, - JdbcConverter converter, NamedParameterJdbcOperations operations, - @Nullable DataAccessStrategy mappingAccessStrategy) { + JdbcConverter converter, NamedParameterJdbcOperations operations) { Assert.notNull(sqlGeneratorSource, "SqlGeneratorSource must not be null"); Assert.notNull(context, "RelationalMappingContext must not be null"); @@ -102,7 +86,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { this.context = context; this.converter = converter; this.operations = operations; - this.accessStrategy = mappingAccessStrategy == null ? this : mappingAccessStrategy; } /* @@ -236,7 +219,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { MapSqlParameterSource parameter = createIdParameterSource(id, domainType); try { - return operations.queryForObject(findOneSql, parameter, (RowMapper) getEntityRowMapper(domainType)); + return operations.queryForObject(findOneSql, parameter, + (RowMapper) getEntityRowMapper(domainType)); } catch (EmptyResultDataAccessException e) { return null; } @@ -249,7 +233,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @SuppressWarnings("unchecked") @Override public Iterable findAll(Class domainType) { - return operations.query(sql(domainType).getFindAll(), (RowMapper) getEntityRowMapper(domainType)); + return operations.query(sql(domainType).getFindAll(), + (RowMapper) getEntityRowMapper(domainType)); } /* @@ -267,7 +252,39 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { String findAllInListSql = sql(domainType).getFindAllInList(); - return operations.query(findAllInListSql, parameterSource, (RowMapper) getEntityRowMapper(domainType)); + return operations.query(findAllInListSql, parameterSource, + (RowMapper) getEntityRowMapper(domainType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.RelationResolver#findAllByPath(org.springframework.data.relational.domain.Identifier, org.springframework.data.mapping.PersistentPropertyPath) + */ + + @SuppressWarnings("unchecked") + @Override + public Iterable findAllByPath(Identifier identifier, + PersistentPropertyPath propertyPath) { + + Assert.notNull(identifier, "identifier must not be null."); + Assert.notNull(propertyPath, "propertyPath must not be null."); + + PersistentPropertyPathExtension path = new PersistentPropertyPathExtension(context, propertyPath); + + Class actualType = path.getActualType(); + String findAllByProperty = sql(actualType) // + .getFindAllByProperty(identifier, path.getQualifierColumn(), path.isOrdered()); + + MapSqlParameterSource parameters = new MapSqlParameterSource(); + + identifier.forEach((name, value, targetType) -> { + parameters.addValue(name, value); + }); + + return operations.query(findAllByProperty, parameters, // + (RowMapper) (path.isMap() // + ? this.getMapEntityRowMapper(path, identifier) // + : this.getEntityRowMapper(path, identifier))); } /* @@ -280,16 +297,9 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { Assert.notNull(rootId, "rootId must not be null."); - Class actualType = property.getActualType(); - String findAllByProperty = sql(actualType) // - .getFindAllByProperty(property.getReverseColumnName(), property.getKeyColumn(), property.isOrdered()); - - MapSqlParameterSource parameter = new MapSqlParameterSource(property.getReverseColumnName(), rootId); - - return operations.query(findAllByProperty, parameter, // - (RowMapper) (property.isMap() // - ? this.getMapEntityRowMapper(property) // - : this.getEntityRowMapper(actualType))); + Class rootType = property.getOwner().getType(); + return findAllByPath(Identifier.of(property.getReverseColumnName(), rootId, rootType), + context.getPersistentPropertyPath(property.getName(), rootType)); } /* @@ -385,15 +395,19 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { } private EntityRowMapper getEntityRowMapper(Class domainType) { - return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), converter, accessStrategy); + return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), converter); } - private RowMapper getMapEntityRowMapper(RelationalPersistentProperty property) { + private EntityRowMapper getEntityRowMapper(PersistentPropertyPathExtension path, Identifier identifier) { + return new EntityRowMapper<>(path, converter, identifier); + } - String keyColumn = property.getKeyColumn(); - Assert.notNull(keyColumn, () -> "KeyColumn must not be null for " + property); + private RowMapper getMapEntityRowMapper(PersistentPropertyPathExtension path, Identifier identifier) { - return new MapEntityRowMapper<>(getEntityRowMapper(property.getActualType()), keyColumn); + String keyColumn = path.getQualifierColumn(); + Assert.notNull(keyColumn, () -> "KeyColumn must not be null for " + path); + + return new MapEntityRowMapper<>(path, converter, identifier, keyColumn); } private MapSqlParameterSource createIdParameterSource(Object id, Class domainType) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java index 83022c0e..99c86127 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java @@ -135,6 +135,15 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy { return delegate.findAllById(ids, domainType); } + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.RelationResolver#findAllByPath(org.springframework.data.relational.domain.Identifier, org.springframework.data.mapping.PersistentPropertyPath) + */ + @Override + public Iterable findAllByPath(Identifier identifier, PersistentPropertyPath path) { + return delegate.findAllByPath(identifier, path); + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllByProperty(java.lang.Object, org.springframework.data.relational.core.mapping.RelationalPersistentProperty) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java index fa5266ae..3a6467fd 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java @@ -17,7 +17,9 @@ package org.springframework.data.jdbc.core.convert; import java.sql.ResultSet; +import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.domain.Identifier; import org.springframework.jdbc.core.RowMapper; /** @@ -34,16 +36,24 @@ import org.springframework.jdbc.core.RowMapper; public class EntityRowMapper implements RowMapper { private final RelationalPersistentEntity entity; - + private final PersistentPropertyPathExtension path; private final JdbcConverter converter; - private final DataAccessStrategy accessStrategy; + private final Identifier identifier; - public EntityRowMapper(RelationalPersistentEntity entity, JdbcConverter converter, - DataAccessStrategy accessStrategy) { + public EntityRowMapper(PersistentPropertyPathExtension path, JdbcConverter converter, Identifier identifier) { + + this.entity = (RelationalPersistentEntity) path.getLeafEntity(); + this.path = path; + this.converter = converter; + this.identifier = identifier; + } + + public EntityRowMapper(RelationalPersistentEntity entity, JdbcConverter converter) { this.entity = entity; + this.path = null; this.converter = converter; - this.accessStrategy = accessStrategy; + this.identifier = null; } /* @@ -52,7 +62,9 @@ public class EntityRowMapper implements RowMapper { */ @Override public T mapRow(ResultSet resultSet, int rowNumber) { - return converter.mapRow(entity, accessStrategy, resultSet); + + return path == null ? converter.mapRow(entity, resultSet, rowNumber) + : converter.mapRow(path, resultSet, identifier, rowNumber); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java index d7c85c1b..08eb1352 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java @@ -18,7 +18,9 @@ package org.springframework.data.jdbc.core.convert; import java.sql.ResultSet; import org.springframework.data.relational.core.conversion.RelationalConverter; +import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.domain.Identifier; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -42,9 +44,7 @@ public interface JdbcConverter extends RelationalConverter { */ JdbcValue writeJdbcValue(@Nullable Object value, Class type, int sqlType); - /* - * (non-Javadoc) - * @see org.springframework.jdbc.core.RowMapper#mapRow(java.sql.ResultSet, int) - */ - T mapRow(RelationalPersistentEntity entity, DataAccessStrategy accessStrategy, ResultSet resultSet); + T mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key); + + T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java index f4b0b319..22610651 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java @@ -20,6 +20,8 @@ import java.sql.SQLException; import java.util.HashMap; import java.util.Map; +import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; +import org.springframework.data.relational.domain.Identifier; import org.springframework.jdbc.core.RowMapper; import org.springframework.lang.NonNull; @@ -32,22 +34,35 @@ import org.springframework.lang.NonNull; */ class MapEntityRowMapper implements RowMapper> { - private final RowMapper delegate; + private final PersistentPropertyPathExtension path; + private final JdbcConverter converter; + private final Identifier identifier; + private final String keyColumn; /** - * @param delegate rowmapper used as a delegate for obtaining the map values. + * @param path + * @param converter + * @param identifier * @param keyColumn the name of the key column. */ - MapEntityRowMapper(RowMapper delegate, String keyColumn) { + MapEntityRowMapper(PersistentPropertyPathExtension path, JdbcConverter converter, + Identifier identifier, String keyColumn) { - this.delegate = delegate; + this.path = path; + this.converter = converter; + this.identifier = identifier; this.keyColumn = keyColumn; } @NonNull @Override public Map.Entry mapRow(ResultSet rs, int rowNum) throws SQLException { - return new HashMap.SimpleEntry<>(rs.getObject(keyColumn), delegate.mapRow(rs, rowNum)); + Object key = rs.getObject(keyColumn); + return new HashMap.SimpleEntry<>(key, mapEntity(rs, key)); + } + + private T mapEntity(ResultSet resultSet, Object key) { + return converter.mapRow(path, resultSet, identifier, key); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RelationResolver.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RelationResolver.java new file mode 100644 index 00000000..ea5bd8e6 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RelationResolver.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 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 + * + * https://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.convert; + +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; + +/** + * Resolves relations within an aggregate. + * + * @author Jens Schauder + * + * @since 1.1 + */ +public interface RelationResolver { + + /** + * Finds all entities reachable via {@literal path}. + * + * @param identifier the combination of Id, map keys and list indexes that identify the parent of the entity to be loaded. Must not be {@literal null}. + * @param path the path from the aggregate root to the entities to be resolved. Must not be {@literal null}. + * @param the type of entity created by this class + * @return Guaranteed to be not {@literal null}. + */ + Iterable findAllByPath(Identifier identifier, + PersistentPropertyPath path); +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index f41ddca9..1b3859ea 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -40,6 +40,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEnti import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.*; import org.springframework.data.relational.core.sql.render.SqlRenderer; +import org.springframework.data.relational.domain.Identifier; import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -163,34 +164,51 @@ class SqlGenerator { * {@literal columnName}. This is used to select values for a complex property ({@link Set}, {@link Map} ...) based on * a referencing entity. * - * @param columnName name of the column of the FK back to the referencing entity. + * @param parentIdentifier 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}. * @return a SQL String. */ - String getFindAllByProperty(String columnName, @Nullable String keyColumn, boolean ordered) { + String getFindAllByProperty(Identifier parentIdentifier, @Nullable String keyColumn, boolean ordered) { Assert.isTrue(keyColumn != null || !ordered, "If the SQL statement should be ordered a keyColumn to order by must be provided."); - SelectBuilder.SelectWhere builder = selectBuilder( - keyColumn == null ? Collections.emptyList() : Collections.singleton(keyColumn)); + SelectBuilder.SelectWhere builder = selectBuilder( // + keyColumn == null // + ? Collections.emptyList() // + : Collections.singleton(keyColumn) // + ); Table table = getTable(); - SelectBuilder.SelectWhereAndOr withWhereClause = builder - .where(table.column(columnName).isEqualTo(getBindMarker(columnName))); - Select select; - if (ordered) { - select = withWhereClause.orderBy(table.column(keyColumn).as(keyColumn)).build(); - } else { - select = withWhereClause.build(); - } + Condition condition = buildConditionForBackReference(parentIdentifier, table); + + SelectBuilder.SelectWhereAndOr withWhereClause = builder.where(condition); + + Select select = ordered // + ? withWhereClause.orderBy(table.column(keyColumn).as(keyColumn)).build() // + : withWhereClause.build(); return render(select); } + private Condition buildConditionForBackReference(Identifier parentIdentifier, Table table) { + + Condition condition = null; + for (String backReferenceColumn : parentIdentifier.toMap().keySet()) { + + Condition newCondition = table.column(backReferenceColumn).isEqualTo(getBindMarker(backReferenceColumn)); + + condition = condition == null ? newCondition : condition.and(newCondition); + } + + Assert.state(condition != null, "We need at least one condition"); + + return condition; + } + /** * Create a {@code SELECT COUNT(id) FROM … WHERE :id = …} statement. * diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 0f0bb693..3ab6cfea 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -22,7 +22,6 @@ import java.util.Map; import org.apache.ibatis.session.SqlSession; import org.mybatis.spring.SqlSessionTemplate; - import org.springframework.data.jdbc.core.convert.CascadingDataAccessStrategy; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; @@ -88,8 +87,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { sqlGeneratorSource, // context, // converter, // - operations, // - cascadingDataAccessStrategy // + operations // ); delegatingDataAccessStrategy.setDelegate(defaultDataAccessStrategy); @@ -244,6 +242,13 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { new MyBatisContext(ids, null, domainType, Collections.emptyMap())); } + @Override + public Iterable findAllByPath(Identifier identifier, + PersistentPropertyPath path) { + return sqlSession().selectList(namespace(path.getBaseProperty().getOwner().getType()) + ".findAllByPath", + new MyBatisContext(identifier, null, path.getLeafProperty().getType(), Collections.emptyMap())); + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllByProperty(java.lang.Object, org.springframework.data.relational.core.mapping.RelationalPersistentProperty) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java index d91bd714..00b9df12 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java @@ -20,7 +20,9 @@ import java.util.Optional; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; @@ -28,6 +30,7 @@ import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.RelationalConverter; @@ -73,8 +76,11 @@ public abstract class AbstractJdbcConfiguration { * @return must not be {@literal null}. */ @Bean - public JdbcConverter jdbcConverter(RelationalMappingContext mappingContext, JdbcOperations operations) { - return new BasicJdbcConverter(mappingContext, jdbcCustomConversions(), new DefaultJdbcTypeFactory(operations)); + public JdbcConverter jdbcConverter(RelationalMappingContext mappingContext, NamedParameterJdbcOperations operations, + @Lazy RelationResolver relationResolver) { + + return new BasicJdbcConverter(mappingContext, relationResolver, jdbcCustomConversions(), + new DefaultJdbcTypeFactory(operations.getJdbcOperations())); } /** @@ -97,16 +103,30 @@ public abstract class AbstractJdbcConfiguration { * @param publisher for publishing events. Must not be {@literal null}. * @param context the mapping context to be used. Must not be {@literal null}. * @param converter the conversions used when reading and writing from/to the database. Must not be {@literal null}. - * @param operations {@link NamedParameterJdbcOperations} used for accessing the database. Must not be - * {@literal null}. * @return a {@link JdbcAggregateTemplate}. Guaranteed to be not {@literal null}. */ @Bean public JdbcAggregateTemplate jdbcAggregateTemplate(ApplicationEventPublisher publisher, - RelationalMappingContext context, JdbcConverter converter, NamedParameterJdbcOperations operations) { + RelationalMappingContext context, JdbcConverter converter, DataAccessStrategy dataAccessStrategy) { - DataAccessStrategy dataAccessStrategy = new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, - converter, operations); return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); } + + /** + * Register a {@link DataAccessStrategy} as a bean for reuse in the {@link JdbcAggregateOperations} and the + * {@link RelationalConverter}. + * + * @param operations + * @param namingStrategy + * @param jdbcConverter + * @return + */ + @Bean + public DataAccessStrategy dataAccessStrategy(NamedParameterJdbcOperations operations, + Optional namingStrategy, JdbcConverter jdbcConverter) { + + JdbcMappingContext context = jdbcMappingContext(namingStrategy); + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, jdbcConverter, operations); + } + } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java index a25512df..36b0b9bd 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java @@ -20,6 +20,7 @@ import java.util.Optional; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.core.convert.converter.Converter; import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; @@ -29,6 +30,7 @@ import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.JdbcTypeFactory; +import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.RelationalConverter; @@ -74,9 +76,9 @@ public class JdbcConfiguration { * @return must not be {@literal null}. */ @Bean - public RelationalConverter relationalConverter(RelationalMappingContext mappingContext) { + public RelationalConverter relationalConverter(RelationalMappingContext mappingContext, @Lazy RelationResolver relationalResolver) { - return new BasicJdbcConverter(mappingContext, jdbcCustomConversions(), JdbcTypeFactory.unsupported()); + return new BasicJdbcConverter(mappingContext, relationalResolver, jdbcCustomConversions(), JdbcTypeFactory.unsupported()); } /** @@ -104,8 +106,23 @@ public class JdbcConfiguration { @Bean public JdbcAggregateOperations jdbcAggregateOperations(ApplicationEventPublisher publisher, RelationalMappingContext context, JdbcConverter converter, NamedParameterJdbcOperations operations) { - DataAccessStrategy dataAccessStrategy = new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, - converter, operations); - return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); + return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy(context, converter, operations)); + } + + /** + * Register a {@link DataAccessStrategy} as a bean for reuse in the {@link JdbcAggregateOperations} and the + * {@link RelationalConverter}. + * + * @param context + * @param converter + * @param operations + * @return + */ + @Bean + public DataAccessStrategy dataAccessStrategy(RelationalMappingContext context, JdbcConverter converter, + NamedParameterJdbcOperations operations) { + + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), + context, converter, operations); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index 8d0ccf62..436fc5c3 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -118,9 +118,8 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { EntityRowMapper defaultEntityRowMapper = new EntityRowMapper<>( // context.getRequiredPersistentEntity(domainType), // - // - converter, // - accessStrategy); + converter // + ); return defaultEntityRowMapper; } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java index 27a6d05b..13143adb 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java @@ -18,17 +18,22 @@ package org.springframework.data.jdbc.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import static org.springframework.data.jdbc.core.PropertyPathTestingUtils.*; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.mapping.PersistentPropertyPath; 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.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.Identifier; +import java.util.List; + /** * Unit tests for {@link DefaultJdbcInterpreter} * @@ -48,9 +53,9 @@ public class DefaultJdbcInterpreterUnitTests { Element element = new Element(); InsertRoot containerInsert = new InsertRoot<>(container); - Insert elementInsert = new Insert<>(element, PropertyPathTestingUtils.toPath("element", Container.class, context), + Insert elementInsert = new Insert<>(element, toPath("element", Container.class, context), containerInsert); - Insert element1Insert = new Insert<>(element, PropertyPathTestingUtils.toPath("element.element1", Container.class, context), + Insert element1Insert = new Insert<>(element, toPath("element.element1", Container.class, context), elementInsert); @Test // DATAJDBC-145 @@ -114,6 +119,41 @@ public class DefaultJdbcInterpreterUnitTests { .containsExactly(tuple(BACK_REFERENCE, CONTAINER_ID, Long.class)); } + @Test // DATAJDBC-223 + public void generateCascadingIds() { + + + ListListContainer listListContainer = new ListListContainer(); + ListContainer listContainer = new ListContainer(); + + InsertRoot listListContainerInsert = new InsertRoot<>(listListContainer); + + PersistentPropertyPath listContainersPath = toPath("listContainers", ListListContainer.class, context); + Insert listContainerInsert = new Insert<>(listContainer, listContainersPath, listListContainerInsert); + listContainerInsert.getQualifiers().put(listContainersPath, 3); + + PersistentPropertyPath listContainersElementsPath = toPath("listContainers.elements", ListListContainer.class, context); + Insert elementInsertInList = new Insert<>(element, listContainersElementsPath, listContainerInsert); + elementInsertInList.getQualifiers().put(listContainersElementsPath, 6); + elementInsertInList.getQualifiers().put(listContainersPath, 3); + + + listListContainerInsert.setGeneratedId(CONTAINER_ID); + + interpreter.interpret(elementInsertInList); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); + verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); + + assertThat(argumentCaptor.getValue().getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( + tuple("list_list_container", CONTAINER_ID, Long.class), // the top level id + tuple("list_list_container_key", 3, Integer.class), // midlevel key + tuple("list_container_key", 6, Integer.class) // lowlevel key + ); + } + @SuppressWarnings("unused") static class Container { @@ -129,4 +169,16 @@ public class DefaultJdbcInterpreterUnitTests { static class Element1 { } + + + static class ListListContainer { + @Id + Long id; + + List listContainers; + } + + private static class ListContainer { + List elements; + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java index ffe40093..cd0dd37c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java @@ -22,11 +22,14 @@ import lombok.Data; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.assertj.core.api.SoftAssertions; +import org.jetbrains.annotations.NotNull; import org.junit.Assume; import org.junit.ClassRule; import org.junit.Rule; @@ -461,8 +464,7 @@ public class JdbcAggregateTemplateIntegrationTests { template.delete(chain4, Chain4.class); - assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM CHAIN0", emptyMap(), Long.class)) // - .isEqualTo(0); + assertThat(count("CHAIN0")).isEqualTo(0); } @Test // DATAJDBC-359 @@ -492,8 +494,224 @@ public class JdbcAggregateTemplateIntegrationTests { template.delete(chain4, NoIdChain4.class); - assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM CHAIN0", emptyMap(), Long.class)) // - .isEqualTo(0); + assertThat(count("CHAIN0")).isEqualTo(0); + } + + @Test // DATAJDBC-223 + public void saveAndLoadLongChainOfListsWithoutIds() { + + NoIdListChain4 saved = template.save(createNoIdTree()); + + assertThat(saved.four).describedAs("Something went wrong during saving").isNotNull(); + + NoIdListChain4 reloaded = template.findById(saved.four, NoIdListChain4.class); + + assertIsUnchanged(saved, reloaded); + + template.deleteById(saved.four, NoIdListChain4.class); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(count("NO_ID_LIST_CHAIN4")).describedAs("Chain4 elements got deleted").isEqualTo(0); + softly.assertThat(count("NO_ID_LIST_CHAIN3")).describedAs("Chain3 elements got deleted").isEqualTo(0); + softly.assertThat(count("NO_ID_LIST_CHAIN2")).describedAs("Chain2 elements got deleted").isEqualTo(0); + softly.assertThat(count("NO_ID_LIST_CHAIN1")).describedAs("Chain1 elements got deleted").isEqualTo(0); + softly.assertThat(count("NO_ID_LIST_CHAIN0")).describedAs("Chain0 elements got deleted").isEqualTo(0); + }); + + } + + /** + * creates an instance of {@link NoIdListChain4} with the following properties: + *
    + *
  • Each element has two children with indices 0 and 1.
  • + *
  • the xxxValue of each element is a {@literal v} followed by the indices used to navigate to the given instance. + *
  • + *
+ * + * @return Guaranteed to be not {@literal null}. + */ + @NotNull + private JdbcAggregateTemplateIntegrationTests.NoIdListChain4 createNoIdTree() { + + NoIdListChain4 chain4 = new NoIdListChain4(); + chain4.fourValue = "v"; + for (int _3 = 0; _3 <= 1; _3++) { + + NoIdListChain3 c3 = new NoIdListChain3(); + c3.threeValue = chain4.fourValue + _3; + chain4.chain3.add(c3); + + for (int _2 = 0; _2 <= 1; _2++) { + + NoIdListChain2 c2 = new NoIdListChain2(); + c2.twoValue = c3.threeValue + _2; + c3.chain2.add(c2); + + for (int _1 = 0; _1 <= 1; _1++) { + + NoIdListChain1 c1 = new NoIdListChain1(); + c1.oneValue = c2.twoValue + _1; + c2.chain1.add(c1); + + for (int _0 = 0; _0 <= 1; _0++) { + + NoIdListChain0 c0 = new NoIdListChain0(); + c0.zeroValue = c1.oneValue + _0; + c1.chain0.add(c0); + + } + } + } + + } + + return chain4; + } + + private void assertIsUnchanged(NoIdListChain4 original, NoIdListChain4 reloaded) { + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(reloaded.fourValue).isEqualTo("v"); + softly.assertThat(reloaded.chain3).hasSize(2); + for (int _3 = 0; _3 <= 1; _3++) { + + NoIdListChain3 c3 = reloaded.chain3.get(_3); + softly.assertThat(c3.threeValue).isEqualTo(original.fourValue + _3); + softly.assertThat(c3.chain2).hasSize(2); + + for (int _2 = 0; _2 <= 1; _2++) { + + NoIdListChain2 c2 = c3.chain2.get(_2); + softly.assertThat(c2.twoValue).isEqualTo(c3.threeValue + _2); + softly.assertThat(c2.chain1).hasSize(2); + + for (int _1 = 0; _1 <= 1; _1++) { + + NoIdListChain1 c1 = c2.chain1.get(_1); + softly.assertThat(c1.oneValue).isEqualTo(c2.twoValue + _1); + softly.assertThat(c1.chain0).hasSize(2); + + for (int _0 = 0; _0 <= 1; _0++) { + + NoIdListChain0 c0 = c1.chain0.get(_0); + softly.assertThat(c0.zeroValue).isEqualTo(c1.oneValue + _0); + + } + } + } + + } + }); + } + + @Test // DATAJDBC-223 + public void saveAndLoadLongChainOfMapsWithoutIds() { + + NoIdMapChain4 saved = template.save(createNoIdMapTree()); + + assertThat(saved.four).isNotNull(); + + NoIdMapChain4 reloaded = template.findById(saved.four, NoIdMapChain4.class); + + assertIsUnchanged(saved, reloaded); + + template.deleteById(saved.four, NoIdMapChain4.class); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(count("NO_ID_MAP_CHAIN4")).describedAs("Chain4 elements got deleted").isEqualTo(0); + softly.assertThat(count("NO_ID_MAP_CHAIN3")).describedAs("Chain3 elements got deleted").isEqualTo(0); + softly.assertThat(count("NO_ID_MAP_CHAIN2")).describedAs("Chain2 elements got deleted").isEqualTo(0); + softly.assertThat(count("NO_ID_MAP_CHAIN1")).describedAs("Chain1 elements got deleted").isEqualTo(0); + softly.assertThat(count("NO_ID_MAP_CHAIN0")).describedAs("Chain0 elements got deleted").isEqualTo(0); + }); + + } + + @NotNull + private JdbcAggregateTemplateIntegrationTests.NoIdMapChain4 createNoIdMapTree() { + + NoIdMapChain4 chain4 = new NoIdMapChain4(); + chain4.fourValue = "v"; + for (int _3 = 0; _3 <= 1; _3++) { + + NoIdMapChain3 c3 = new NoIdMapChain3(); + c3.threeValue = chain4.fourValue + _3; + chain4.chain3.put(asString(_3), c3); + + for (int _2 = 0; _2 <= 1; _2++) { + + NoIdMapChain2 c2 = new NoIdMapChain2(); + c2.twoValue = c3.threeValue + _2; + c3.chain2.put(asString(_2), c2); + + for (int _1 = 0; _1 <= 1; _1++) { + + NoIdMapChain1 c1 = new NoIdMapChain1(); + c1.oneValue = c2.twoValue + _1; + c2.chain1.put(asString(_1), c1); + + for (int _0 = 0; _0 <= 1; _0++) { + + NoIdMapChain0 c0 = new NoIdMapChain0(); + c0.zeroValue = c1.oneValue + _0; + c1.chain0.put(asString(_0), c0); + + } + } + } + + } + return chain4; + } + + private void assertIsUnchanged(NoIdMapChain4 original, NoIdMapChain4 reloaded) { + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(reloaded.fourValue).isEqualTo("v"); + softly.assertThat(reloaded.chain3).hasSize(2); + for (int _3 = 0; _3 <= 1; _3++) { + + NoIdMapChain3 c3 = reloaded.chain3.get(asString(_3)); + softly.assertThat(c3.threeValue).isEqualTo(original.fourValue + _3); + softly.assertThat(c3.chain2).hasSize(2); + + for (int _2 = 0; _2 <= 1; _2++) { + + NoIdMapChain2 c2 = c3.chain2.get(asString(_2)); + softly.assertThat(c2.twoValue).isEqualTo(c3.threeValue + _2); + softly.assertThat(c2.chain1).hasSize(2); + + for (int _1 = 0; _1 <= 1; _1++) { + + NoIdMapChain1 c1 = c2.chain1.get(asString(_1)); + softly.assertThat(c1.oneValue).isEqualTo(c2.twoValue + _1); + softly.assertThat(c1.chain0).hasSize(2); + + for (int _0 = 0; _0 <= 1; _0++) { + + NoIdMapChain0 c0 = c1.chain0.get(asString(_0)); + softly.assertThat(c0.zeroValue).isEqualTo(c1.oneValue + _0); + + } + } + } + + } + + }); + } + + private static String asString(int i) { + + return "_" + i; + } + + private Long count(String tableName) { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM " + tableName, emptyMap(), Long.class); } private static void assumeNot(String dbProfileName) { @@ -659,4 +877,60 @@ public class JdbcAggregateTemplateIntegrationTests { String fourValue; NoIdChain3 chain3; } + + /** + * One may think of ChainN as a chain with N further elements + */ + static class NoIdListChain0 { + String zeroValue; + } + + static class NoIdListChain1 { + String oneValue; + List chain0 = new ArrayList<>(); + } + + static class NoIdListChain2 { + String twoValue; + List chain1 = new ArrayList<>(); + } + + static class NoIdListChain3 { + String threeValue; + List chain2 = new ArrayList<>(); + } + + static class NoIdListChain4 { + @Id Long four; + String fourValue; + List chain3 = new ArrayList<>(); + } + + /** + * One may think of ChainN as a chain with N further elements + */ + static class NoIdMapChain0 { + String zeroValue; + } + + static class NoIdMapChain1 { + String oneValue; + Map chain0 = new HashMap<>(); + } + + static class NoIdMapChain2 { + String twoValue; + Map chain1 = new HashMap<>(); + } + + static class NoIdMapChain3 { + String threeValue; + Map chain2 = new HashMap<>(); + } + + static class NoIdMapChain4 { + @Id Long four; + String fourValue; + Map chain3 = new HashMap<>(); + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java index fe1076b3..b0d65df3 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java @@ -16,12 +16,10 @@ package org.springframework.data.jdbc.core.convert; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import org.assertj.core.api.Assertions; -import org.assertj.core.api.SoftAssertions; import org.junit.Test; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -39,7 +37,7 @@ import org.springframework.data.util.ClassTypeInformation; public class BasicRelationalConverterAggregateReferenceUnitTests { JdbcMappingContext context = new JdbcMappingContext(); - RelationalConverter converter = new BasicJdbcConverter(context); + RelationalConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class)); RelationalPersistentEntity entity = context.getRequiredPersistentEntity(DummyEntity.class); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java index 0b194329..fcb1a810 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java @@ -25,18 +25,13 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; import java.util.HashMap; +import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; -import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; -import org.springframework.data.jdbc.core.convert.JdbcConverter; -import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; -import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.jdbc.core.JdbcOperations; @@ -58,16 +53,27 @@ public class DefaultDataAccessStrategyUnitTests { NamedParameterJdbcOperations namedJdbcOperations = mock(NamedParameterJdbcOperations.class); JdbcOperations jdbcOperations = mock(JdbcOperations.class); RelationalMappingContext context = new JdbcMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(context, new JdbcCustomConversions(), - new DefaultJdbcTypeFactory(jdbcOperations)); + HashMap additionalParameters = new HashMap<>(); ArgumentCaptor paramSourceCaptor = ArgumentCaptor.forClass(SqlParameterSource.class); - DefaultDataAccessStrategy accessStrategy = new DefaultDataAccessStrategy( // - new SqlGeneratorSource(context), // - context, // - converter, // - namedJdbcOperations); + JdbcConverter converter; + DefaultDataAccessStrategy accessStrategy; + + @Before + public void before() { + DelegatingDataAccessStrategy relationResolver = new DelegatingDataAccessStrategy(); + + converter = new BasicJdbcConverter(context, relationResolver, + new JdbcCustomConversions(), new DefaultJdbcTypeFactory(jdbcOperations)); + accessStrategy = new DefaultDataAccessStrategy( // + new SqlGeneratorSource(context), // + context, // + converter, // + namedJdbcOperations); + + relationResolver.setDelegate(accessStrategy); + } @Test // DATAJDBC-146 public void additionalParameterForIdDoesNotLeadToDuplicateParameters() { @@ -100,7 +106,9 @@ public class DefaultDataAccessStrategyUnitTests { @Test // DATAJDBC-235 public void considersConfiguredWriteConverter() { - JdbcConverter converter = new BasicJdbcConverter(context, + DelegatingDataAccessStrategy relationResolver = new DelegatingDataAccessStrategy(); + + JdbcConverter converter = new BasicJdbcConverter(context, relationResolver, new JdbcCustomConversions(Arrays.asList(BooleanToStringConverter.INSTANCE, StringToBooleanConverter.INSTANCE)), new DefaultJdbcTypeFactory(jdbcOperations)); @@ -110,6 +118,8 @@ public class DefaultDataAccessStrategyUnitTests { converter, // namedJdbcOperations); + relationResolver.setDelegate(accessStrategy); + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); EntityWithBoolean entity = new EntityWithBoolean(ORIGINAL_ID, true); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java index 2004a335..8dbb2163 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java @@ -18,8 +18,7 @@ package org.springframework.data.jdbc.core.convert; import static java.util.Arrays.*; import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import lombok.AllArgsConstructor; @@ -45,16 +44,19 @@ import javax.naming.OperationNotSupportedException; import org.assertj.core.api.SoftAssertions; import org.junit.Test; +import org.mockito.ArgumentMatchers; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.Embedded; 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.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; import org.springframework.data.repository.query.Param; import org.springframework.util.Assert; @@ -580,32 +582,52 @@ public class EntityRowMapperUnitTests { DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class); // the ID of the entity is used to determine what kind of ResultSet is needed for subsequent selects. - doReturn(new HashSet<>(asList(new Trivial(1L, "one"), new Trivial(2L, "two")))).when(accessStrategy) - .findAllByProperty(eq(ID_FOR_ENTITY_NOT_REFERENCING_MAP), any(RelationalPersistentProperty.class)); + HashSet trivials = new HashSet<>(asList( // + new Trivial(1L, "one"), // + new Trivial(2L, "two") // + )); - doReturn(new HashSet<>(asList( // + HashSet> simpleEntriesWithInts = new HashSet<>(asList( // + new SimpleEntry<>(1, new Trivial(1L, "one")), // + new SimpleEntry<>(2, new Trivial(2L, "two")) // + )); + + HashSet> simpleEntriesWithStringKeys = new HashSet<>(asList( // new SimpleEntry<>("one", new Trivial(1L, "one")), // new SimpleEntry<>("two", new Trivial(2L, "two")) // - ))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_MAP), + )); + + doReturn(trivials).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_NOT_REFERENCING_MAP), any(RelationalPersistentProperty.class)); - doReturn(new HashSet<>(asList( // - new SimpleEntry<>(1, new Trivial(1L, "one")), // - new SimpleEntry<>(2, new Trivial(2L, "tow")) // - ))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_LIST), + doReturn(simpleEntriesWithStringKeys).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_MAP), any(RelationalPersistentProperty.class)); - JdbcConverter converter = new BasicJdbcConverter(context, new JdbcCustomConversions(), + doReturn(simpleEntriesWithInts).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_LIST), + any(RelationalPersistentProperty.class)); + + doReturn(trivials).when(accessStrategy).findAllByPath(identifierOfValue(ID_FOR_ENTITY_NOT_REFERENCING_MAP), + any(PersistentPropertyPath.class)); + + doReturn(simpleEntriesWithStringKeys).when(accessStrategy) + .findAllByPath(identifierOfValue(ID_FOR_ENTITY_REFERENCING_MAP), any(PersistentPropertyPath.class)); + + doReturn(simpleEntriesWithInts).when(accessStrategy) + .findAllByPath(identifierOfValue(ID_FOR_ENTITY_REFERENCING_LIST), any(PersistentPropertyPath.class)); + + BasicJdbcConverter converter = new BasicJdbcConverter(context, accessStrategy, new JdbcCustomConversions(), JdbcTypeFactory.unsupported()); return new EntityRowMapper<>( // (RelationalPersistentEntity) context.getRequiredPersistentEntity(type), // - // - converter, // - accessStrategy // + converter // ); } + private Identifier identifierOfValue(long value) { + return ArgumentMatchers.argThat(argument -> argument.toMap().containsValue(value)); + } + private static ResultSet mockResultSet(List columns, Object... values) { Assert.isTrue( // diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index aa15d93b..4ca8aa89 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -39,6 +39,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEnti import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.Aliased; import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.domain.Identifier; /** * Unit tests for the {@link SqlGenerator}. @@ -51,6 +52,8 @@ import org.springframework.data.relational.core.sql.Table; */ public class SqlGeneratorUnitTests { + static final Identifier BACKREF = Identifier.of("backref", "some-value", String.class); + SqlGenerator sqlGenerator; NamingStrategy namingStrategy = new PrefixingNamingStrategy(); RelationalMappingContext context = new JdbcMappingContext(namingStrategy); @@ -149,7 +152,7 @@ public class SqlGeneratorUnitTests { public void findAllByProperty() { // this would get called when ListParent is the element type of a Set - String sql = sqlGenerator.getFindAllByProperty("backref", null, false); + String sql = sqlGenerator.getFindAllByProperty(BACKREF, null, false); assertThat(sql).contains("SELECT", // "dummy_entity.id1 AS id1", // @@ -165,11 +168,33 @@ public class SqlGeneratorUnitTests { "WHERE dummy_entity.backref = :backref"); } + @Test // DATAJDBC-223 + public void findAllByPropertyWithMultipartIdentifier() { + + // this would get called when ListParent is the element type of a Set + String sql = sqlGenerator.getFindAllByProperty(Identifier.of("backref", "some-value", String.class).withPart("backref_key", "key-value", Object.class), null, false); + + assertThat(sql).contains("SELECT", // + "dummy_entity.id1 AS id1", // + "dummy_entity.x_name AS x_name", // + "dummy_entity.x_other AS x_other", // + "ref.x_l1id AS ref_x_l1id", // + "ref.x_content AS ref_x_content", // + "ref_further.x_l2id AS ref_further_x_l2id", // + "ref_further.x_something AS ref_further_x_something", // + "FROM dummy_entity ", // + "LEFT OUTER JOIN referenced_entity AS ref ON ref.dummy_entity = dummy_entity.id1", // + "LEFT OUTER JOIN second_level_referenced_entity AS ref_further ON ref_further.referenced_entity = ref.x_l1id", // + "dummy_entity.backref = :backref", + "dummy_entity.backref_key = :backref_key" + ); + } + @Test // DATAJDBC-131, DATAJDBC-111 public void findAllByPropertyWithKey() { // this would get called when ListParent is th element type of a Map - String sql = sqlGenerator.getFindAllByProperty("backref", "key-column", false); + String sql = sqlGenerator.getFindAllByProperty(BACKREF, "key-column", false); assertThat(sql).isEqualTo("SELECT dummy_entity.id1 AS id1, dummy_entity.x_name AS x_name, " // + "dummy_entity.x_other AS x_other, " // @@ -184,14 +209,14 @@ public class SqlGeneratorUnitTests { @Test(expected = IllegalArgumentException.class) // DATAJDBC-130 public void findAllByPropertyOrderedWithoutKey() { - sqlGenerator.getFindAllByProperty("back-ref", null, true); + sqlGenerator.getFindAllByProperty(BACKREF, null, true); } @Test // DATAJDBC-131, DATAJDBC-111 public void findAllByPropertyWithKeyOrdered() { // this would get called when ListParent is th element type of a Map - String sql = sqlGenerator.getFindAllByProperty("backref", "key-column", true); + String sql = sqlGenerator.getFindAllByProperty(BACKREF, "key-column", true); assertThat(sql).isEqualTo("SELECT dummy_entity.id1 AS id1, dummy_entity.x_name AS x_name, " // + "dummy_entity.x_other AS x_other, " // @@ -297,7 +322,7 @@ public class SqlGeneratorUnitTests { final SqlGenerator sqlGenerator = createSqlGenerator(EntityWithReadOnlyProperty.class); - assertThat(sqlGenerator.getFindAllByProperty("backref", "key-column", true)).isEqualToIgnoringCase( // + assertThat(sqlGenerator.getFindAllByProperty(BACKREF, "key-column", true)).isEqualToIgnoringCase( // "SELECT " // + "entity_with_read_only_property.x_id AS x_id, " // + "entity_with_read_only_property.x_name AS x_name, " // diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index 61cbcd18..c565c8a6 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -38,6 +38,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; +import org.springframework.data.jdbc.core.convert.DelegatingDataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; @@ -77,11 +78,13 @@ public class SimpleJdbcRepositoryEventsUnitTests { RelationalMappingContext context = new JdbcMappingContext(); NamedParameterJdbcOperations operations = createIdGeneratingOperations(); - JdbcConverter converter = new BasicJdbcConverter(context, new JdbcCustomConversions(), + DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy(); + JdbcConverter converter = new BasicJdbcConverter(context, delegatingDataAccessStrategy, new JdbcCustomConversions(), new DefaultJdbcTypeFactory(operations.getJdbcOperations())); SqlGeneratorSource generatorSource = new SqlGeneratorSource(context); this.dataAccessStrategy = spy(new DefaultDataAccessStrategy(generatorSource, context, converter, operations)); + delegatingDataAccessStrategy.setDelegate(dataAccessStrategy); JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter, publisher, operations); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java index 723d8f28..3a00809a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java @@ -84,7 +84,7 @@ public class JdbcRepositoryFactoryBeanUnitTests { factoryBean.setDataAccessStrategy(dataAccessStrategy); factoryBean.setMappingContext(mappingContext); - factoryBean.setConverter(new BasicJdbcConverter(mappingContext, JdbcTypeFactory.unsupported())); + factoryBean.setConverter(new BasicJdbcConverter(mappingContext, dataAccessStrategy)); factoryBean.setApplicationEventPublisher(publisher); factoryBean.setBeanFactory(beanFactory); factoryBean.afterPropertiesSet(); @@ -111,7 +111,7 @@ public class JdbcRepositoryFactoryBeanUnitTests { public void afterPropertiesSetDefaultsNullablePropertiesCorrectly() { factoryBean.setMappingContext(mappingContext); - factoryBean.setConverter(new BasicJdbcConverter(mappingContext, JdbcTypeFactory.unsupported())); + factoryBean.setConverter(new BasicJdbcConverter(mappingContext, dataAccessStrategy)); factoryBean.setApplicationEventPublisher(publisher); factoryBean.setBeanFactory(beanFactory); factoryBean.afterPropertiesSet(); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 03b7773c..5121d07e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -20,13 +20,13 @@ import java.util.Optional; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; @@ -34,6 +34,7 @@ import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; @@ -81,7 +82,11 @@ public class TestConfiguration { DataAccessStrategy defaultDataAccessStrategy( @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, RelationalMappingContext context, JdbcConverter converter) { - return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter, template); + + DefaultDataAccessStrategy defaultDataAccessStrategy = new DefaultDataAccessStrategy(new SqlGeneratorSource(context), + context, converter, template); + + return defaultDataAccessStrategy; } @Bean @@ -98,9 +103,14 @@ public class TestConfiguration { } @Bean - JdbcConverter relationalConverter(RelationalMappingContext mappingContext, CustomConversions conversions, - @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template) { - return new BasicJdbcConverter(mappingContext, conversions, - new DefaultJdbcTypeFactory(template.getJdbcOperations())); + JdbcConverter relationalConverter(RelationalMappingContext mappingContext, @Lazy RelationResolver relationResolver, + CustomConversions conversions, @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template) { + + return new BasicJdbcConverter( // + mappingContext, // + relationResolver, // + conversions, // + new DefaultJdbcTypeFactory(template.getJdbcOperations()) // + ); } } diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql index a2da8d67..bd14282b 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql @@ -100,28 +100,196 @@ CREATE TABLE NO_ID_CHAIN4 CREATE TABLE NO_ID_CHAIN3 ( - THREE_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, + THREE_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_CHAIN2 ( - TWO_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, + TWO_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_CHAIN1 ( - ONE_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, + ONE_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_CHAIN0 ( - ZERO_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, + ZERO_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); + + +CREATE TABLE NO_ID_LIST_CHAIN4 +( + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_LIST_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY), + FOREIGN KEY (NO_ID_LIST_CHAIN4) REFERENCES NO_ID_LIST_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_LIST_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) REFERENCES NO_ID_LIST_CHAIN3 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) REFERENCES NO_ID_LIST_CHAIN2 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + NO_ID_LIST_CHAIN1_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY, + NO_ID_LIST_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) REFERENCES NO_ID_LIST_CHAIN1 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) +); + + + + +CREATE TABLE NO_ID_MAP_CHAIN4 +( + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_MAP_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY), + FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_MAP_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) REFERENCES NO_ID_MAP_CHAIN3 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) REFERENCES NO_ID_MAP_CHAIN2 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + NO_ID_MAP_CHAIN1_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY, + NO_ID_MAP_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) REFERENCES NO_ID_MAP_CHAIN1 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql index 526dccd6..6d538bb1 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql @@ -116,3 +116,171 @@ CREATE TABLE NO_ID_CHAIN0 NO_ID_CHAIN4 BIGINT, FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); + + +CREATE TABLE NO_ID_LIST_CHAIN4 +( + FOUR BIGINT AUTO_INCREMENT PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_LIST_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY), + FOREIGN KEY (NO_ID_LIST_CHAIN4) REFERENCES NO_ID_LIST_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_LIST_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) REFERENCES NO_ID_LIST_CHAIN3 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) REFERENCES NO_ID_LIST_CHAIN2 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + NO_ID_LIST_CHAIN1_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY, + NO_ID_LIST_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) REFERENCES NO_ID_LIST_CHAIN1 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) +); + + + + +CREATE TABLE NO_ID_MAP_CHAIN4 +( + FOUR BIGINT AUTO_INCREMENT PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_MAP_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY), + FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_MAP_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) REFERENCES NO_ID_MAP_CHAIN3 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) REFERENCES NO_ID_MAP_CHAIN2 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + NO_ID_MAP_CHAIN1_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY, + NO_ID_MAP_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) REFERENCES NO_ID_MAP_CHAIN1 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql index 94b6aef1..9048d9df 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql @@ -125,3 +125,170 @@ CREATE TABLE NO_ID_CHAIN0 NO_ID_CHAIN4 BIGINT, FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); + +CREATE TABLE NO_ID_LIST_CHAIN4 +( + FOUR BIGINT IDENTITY PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_LIST_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY), + FOREIGN KEY (NO_ID_LIST_CHAIN4) REFERENCES NO_ID_LIST_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_LIST_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) REFERENCES NO_ID_LIST_CHAIN3 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) REFERENCES NO_ID_LIST_CHAIN2 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + NO_ID_LIST_CHAIN1_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY, + NO_ID_LIST_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) REFERENCES NO_ID_LIST_CHAIN1 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) +); + + + + +CREATE TABLE NO_ID_MAP_CHAIN4 +( + FOUR BIGINT IDENTITY PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_MAP_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY), + FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_MAP_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) REFERENCES NO_ID_MAP_CHAIN3 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) REFERENCES NO_ID_MAP_CHAIN2 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + NO_ID_MAP_CHAIN1_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY, + NO_ID_MAP_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) REFERENCES NO_ID_MAP_CHAIN1 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql index 526dccd6..5747ba0b 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql @@ -116,3 +116,170 @@ CREATE TABLE NO_ID_CHAIN0 NO_ID_CHAIN4 BIGINT, FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); + +CREATE TABLE NO_ID_LIST_CHAIN4 +( + FOUR BIGINT AUTO_INCREMENT PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_LIST_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY), + FOREIGN KEY (NO_ID_LIST_CHAIN4) REFERENCES NO_ID_LIST_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_LIST_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) REFERENCES NO_ID_LIST_CHAIN3 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) REFERENCES NO_ID_LIST_CHAIN2 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + NO_ID_LIST_CHAIN1_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY, + NO_ID_LIST_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) REFERENCES NO_ID_LIST_CHAIN1 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) +); + + + + +CREATE TABLE NO_ID_MAP_CHAIN4 +( + FOUR BIGINT AUTO_INCREMENT PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_MAP_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY), + FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_MAP_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) REFERENCES NO_ID_MAP_CHAIN3 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) REFERENCES NO_ID_MAP_CHAIN2 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + NO_ID_MAP_CHAIN1_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY, + NO_ID_MAP_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) REFERENCES NO_ID_MAP_CHAIN1 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql index c8ccc0cd..20fbd20a 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql @@ -136,3 +136,171 @@ CREATE TABLE NO_ID_CHAIN0 NO_ID_CHAIN4 BIGINT, FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); + + +CREATE TABLE NO_ID_LIST_CHAIN4 +( + FOUR SERIAL PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_LIST_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY), + FOREIGN KEY (NO_ID_LIST_CHAIN4) REFERENCES NO_ID_LIST_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_LIST_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) REFERENCES NO_ID_LIST_CHAIN3 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) REFERENCES NO_ID_LIST_CHAIN2 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_LIST_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + NO_ID_LIST_CHAIN1_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY, + NO_ID_LIST_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) REFERENCES NO_ID_LIST_CHAIN1 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) +); + + + + +CREATE TABLE NO_ID_MAP_CHAIN4 +( + FOUR SERIAL PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_MAP_CHAIN3 +( + THREE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY), + FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_MAP_CHAIN2 +( + TWO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) REFERENCES NO_ID_MAP_CHAIN3 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN1 +( + ONE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) REFERENCES NO_ID_MAP_CHAIN2 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) +); + +CREATE TABLE NO_ID_MAP_CHAIN0 +( + ZERO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + NO_ID_MAP_CHAIN1_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY, + NO_ID_MAP_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) REFERENCES NO_ID_MAP_CHAIN1 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) +); \ No newline at end of file diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/PathNode.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/PathNode.java index 40dbe8b0..e356889a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/PathNode.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/PathNode.java @@ -16,13 +16,15 @@ package org.springframework.data.relational.core.conversion; import lombok.Value; + import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.util.Pair; import org.springframework.lang.Nullable; /** - * 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. + * 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. * * @author Jens Schauder */ @@ -37,11 +39,23 @@ class PathNode { /** * The parent {@link PathNode}. This is {@code null} if this is the root entity. */ - @Nullable - PathNode parent; + @Nullable PathNode parent; /** * The value of the entity. */ Object value; + + /** + * If the node represents a qualified property (i.e. a {@link java.util.List} or {@link java.util.Map}) the actual + * value is an element of the {@literal List} or a value of the {@literal Map}, while the {@link #value} is actually a + * {@link Pair} with the index or key as the first element and the actual value as second element. + * + */ + Object getActualValue() { + + return getPath().getRequiredLeafProperty().isQualified() // + ? ((Pair) getValue()).getSecond() // + : getValue(); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalPropertyPath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalPropertyPath.java deleted file mode 100644 index a9f708de..00000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalPropertyPath.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2017-2019 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 - * - * https://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.mapping.PropertyPath; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * A replacement for {@link org.springframework.data.mapping.PropertyPath} as long as it doesn't support objects with - * empty path. See https://jira.spring.io/browse/DATACMNS-1204. - * - * @author Jens Schauder - */ -public class RelationalPropertyPath { - - private final PropertyPath path; - private final Class rootType; - - RelationalPropertyPath(PropertyPath path) { - - Assert.notNull(path, "path must not be null if rootType is not set"); - - this.path = path; - this.rootType = null; - } - - private RelationalPropertyPath(Class type) { - - Assert.notNull(type, "type must not be null if path is not set"); - - this.path = null; - this.rootType = type; - } - - public static RelationalPropertyPath from(String source, Class type) { - - if (StringUtils.isEmpty(source)) { - return new RelationalPropertyPath(type); - } else { - return new RelationalPropertyPath(PropertyPath.from(source, type)); - } - } - - public RelationalPropertyPath nested(String name) { - - return path == null ? // - new RelationalPropertyPath(PropertyPath.from(name, rootType)) // - : new RelationalPropertyPath(path.nested(name)); - } - - public PropertyPath getPath() { - return path; - } - - public String toDotPath() { - return path == null ? "" : path.toDotPath(); - } -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java index 06da9f47..faca7bd1 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java @@ -25,6 +25,7 @@ import java.util.Map; 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.Pair; import org.springframework.lang.Nullable; @@ -121,16 +122,23 @@ class WritingContext { from(path).forEach(node -> { + DbAction.WithEntity parentAction = getAction(node.getParent()); DbAction.Insert insert; if (node.getPath().getRequiredLeafProperty().isQualified()) { @SuppressWarnings("unchecked") Pair value = (Pair) node.getValue(); - insert = new DbAction.Insert<>(value.getSecond(), path, getAction(node.getParent())); + insert = new DbAction.Insert<>(value.getSecond(), path, parentAction); insert.getQualifiers().put(node.getPath(), value.getFirst()); + RelationalPersistentEntity parentEntity = context.getRequiredPersistentEntity(parentAction.getEntityType()); + + if (!parentEntity.hasIdProperty() && parentAction instanceof DbAction.Insert) { + insert.getQualifiers().putAll(((DbAction.Insert) parentAction).getQualifiers()); + } + } else { - insert = new DbAction.Insert<>(node.getValue(), path, getAction(node.getParent())); + insert = new DbAction.Insert<>(node.getValue(), path, parentAction); } previousActions.put(node, insert); actions.add(insert); @@ -182,10 +190,6 @@ class WritingContext { return null; } - // commented as of #DATAJDBC-282 - // private boolean isNew(Object o) { - // return context.getRequiredPersistentEntity(o.getClass()).isNew(o); - // } private List from(PersistentPropertyPath path) { @@ -201,7 +205,10 @@ class WritingContext { List pathNodes = nodesCache.get(path.getParentPath()); pathNodes.forEach(parentNode -> { - Object value = path.getRequiredLeafProperty().getOwner().getPropertyAccessor(parentNode.getValue()) + // todo: this should go into pathnode + Object parentValue = parentNode.getActualValue(); + + Object value = path.getRequiredLeafProperty().getOwner().getPropertyAccessor(parentValue) .getProperty(path.getRequiredLeafProperty()); nodes.addAll(createNodes(path, parentNode, value)); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java index 1cdccf2c..85ef2c2c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java @@ -17,6 +17,7 @@ package org.springframework.data.relational.core.mapping; import lombok.EqualsAndHashCode; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; @@ -24,7 +25,7 @@ import org.springframework.util.Assert; /** * A wrapper around a {@link org.springframework.data.mapping.PersistentPropertyPath} for making common operations - * available used in SQL generation. + * available used in SQL generation and conversion * * @author Jens Schauder * @since 1.1 @@ -36,6 +37,12 @@ public class PersistentPropertyPathExtension { private final @Nullable PersistentPropertyPath path; private final MappingContext, RelationalPersistentProperty> context; + /** + * Creates the empty path referencing the root itself. + * + * @param context Must not be {@literal null}. + * @param entity Root entity of the path. Must not be {@literal null}. + */ public PersistentPropertyPathExtension( MappingContext, RelationalPersistentProperty> context, RelationalPersistentEntity entity) { @@ -48,6 +55,12 @@ public class PersistentPropertyPathExtension { this.path = null; } + /** + * Creates a non empty path + * + * @param context Must not be {@literal null}. + * @param path Must not be {@literal null}. + */ public PersistentPropertyPathExtension( MappingContext, RelationalPersistentProperty> context, PersistentPropertyPath path) { @@ -188,6 +201,11 @@ public class PersistentPropertyPathExtension { return leafEntity != null && leafEntity.hasIdProperty(); } + /** + * Returns the longest ancestor path that has an Id property. + * + * @return A path that starts just as this path but is shorter. Guaranteed to be not {@literal null}. + */ public PersistentPropertyPathExtension getIdDefiningParentPath() { PersistentPropertyPathExtension parent = getParentPath(); @@ -247,6 +265,112 @@ public class PersistentPropertyPathExtension { return path == null ? 0 : path.getLength(); } + /** + * Tests if {@code this} and the argument represent the same path. + * + * @param path to which this path gets compared. May be {@literal null}. + * @return Whence the argument matches the path represented by this instance. + */ + public boolean matches(PersistentPropertyPath path) { + return this.path == null ? path.isEmpty() : this.path.equals(path); + } + + /** + * The id property of the final element of the path. + * + * @return Guaranteed to be not {@literal null}. + * @throws IllegalStateException if no such property exists. + */ + public RelationalPersistentProperty getRequiredIdProperty() { + return this.path == null ? entity.getRequiredIdProperty() : getRequiredLeafEntity().getRequiredIdProperty(); + } + + /** + * The column name used for the list index or map key of the leaf property of this path. + * + * @return May be {@literal null}. + */ + @Nullable + public String getQualifierColumn() { + return path == null ? "" : path.getRequiredLeafProperty().getKeyColumn(); + } + + /** + * The type of the qualifier column of the leaf property of this path or {@literal null} if this is not applicable. + * + * @return May be {@literal null}. + */ + @Nullable + public Class getQualifierColumnType() { + return path == null ? null : path.getRequiredLeafProperty().getQualifierColumnType(); + } + + /** + * Creates a new path by extending the current path by the property passed as an argument. + * + * @param property Must not be {@literal null}. + * @return Guaranteed to be not {@literal null}. + */ + public PersistentPropertyPathExtension extendBy(RelationalPersistentProperty property) { + + PersistentPropertyPath newPath; + if (path == null) { + newPath = context.getPersistentPropertyPath(property.getName(), entity.getType()); + } else { + newPath = context.getPersistentPropertyPath(path.toDotPath() + "." + property.getName(), entity.getType()); + } + + return new PersistentPropertyPathExtension(context, newPath); + } + + @Override + public String toString() { + return String.format("PersistentPropertyPathExtension[%s, %s]", entity.getName(), + path == null ? "-" : path.toDotPath()); + } + + /** + * For empty paths this is the type of the entity. For non empty paths this is the actual type of the leaf property. + * + * @return Guaranteed to be not {@literal null}. + * @see PersistentProperty#getActualType() + */ + public Class getActualType() { + + return path == null // + ? entity.getType() // + : path.getRequiredLeafProperty().getActualType(); + } + + /** + * @return whether the leaf end of the path is ordered, i.e. the data to populate must be ordered. + * @see RelationalPersistentProperty#isOrdered() + */ + public boolean isOrdered() { + return path != null && path.getRequiredLeafProperty().isOrdered(); + } + + /** + * @return {@literal true} if the leaf property of this path is a {@link java.util.Map}. + * @see RelationalPersistentProperty#isMap() + */ + public boolean isMap() { + return path != null && path.getRequiredLeafProperty().isMap(); + } + + /** + * Converts this path to a non-null {@link PersistentPropertyPath}. + * + * @return Guaranteed to be not {@literal null}. + * @throws IllegalStateException if this path is empty. + */ + public PersistentPropertyPath getRequiredPersistentPropertyPath() { + + Assert.state(path != null, "No path."); + + return path; + } + /** * Finds and returns the longest path with ich identical or an ancestor to the current path and maps directly to a * table. @@ -291,7 +415,6 @@ public class PersistentPropertyPathExtension { return getParentPath().assembleColumnName(embeddedPrefix + suffix); } - @SuppressWarnings("unchecked") private RelationalPersistentEntity getRequiredLeafEntity() { return path == null ? entity : context.getRequiredPersistentEntity(path.getRequiredLeafProperty().getActualType()); } @@ -302,37 +425,4 @@ public class PersistentPropertyPathExtension { return tableAlias == null ? columnName : tableAlias + "_" + columnName; } - public boolean matches(PersistentPropertyPath path) { - return this.path == null ? path.isEmpty() : this.path.equals(path); - } - - public RelationalPersistentProperty getRequiredIdProperty() { - return this.path == null ? entity.getRequiredIdProperty() : getLeafEntity().getRequiredIdProperty(); - } - - public String getKeyColumn() { - return path == null ? "" : path.getRequiredLeafProperty().getKeyColumn(); - } - - public Class getQualifierColumnType() { - return path == null ? null : path.getRequiredLeafProperty().getQualifierColumnType(); - } - - public PersistentPropertyPathExtension extendBy(RelationalPersistentProperty property) { - - PersistentPropertyPath newPath; - if (path == null) { - newPath = context.getPersistentPropertyPath(property.getName(), entity.getType()); - } else { - newPath = context.getPersistentPropertyPath(path.toDotPath() + "." + property.getName(), entity.getType()); - } - - return new PersistentPropertyPathExtension(context, newPath); - } - - @Override - public String toString() { - return String.format("PersistentPropertyPathExtension[%s, %s]", entity.getName(), - path == null ? "-" : path.toDotPath()); - } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java index c196d659..d1206999 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java @@ -40,6 +40,7 @@ import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.lang.Nullable; /** * Unit tests for the {@link RelationalEntityWriter} @@ -54,9 +55,23 @@ public class RelationalEntityWriterUnitTests { final RelationalMappingContext context = new RelationalMappingContext(); final RelationalEntityWriter converter = new RelationalEntityWriter(context); - final PersistentPropertyPath listContainerElements = toPath("elements", ListContainer.class, context); + final PersistentPropertyPath listContainerElements = toPath("elements", + ListContainer.class, context); - private final PersistentPropertyPath mapContainerElements = toPath("elements", MapContainer.class, context); + private final PersistentPropertyPath mapContainerElements = toPath("elements", + MapContainer.class, context); + + private final PersistentPropertyPath listMapContainerElements = toPath("maps.elements", + ListMapContainer.class, context); + + private final PersistentPropertyPath listMapContainerMaps = toPath("maps", + ListMapContainer.class, context); + + private final PersistentPropertyPath noIdListMapContainerElements = toPath("maps.elements", + NoIdListMapContainer.class, context); + + private final PersistentPropertyPath noIdListMapContainerMaps = toPath("maps", + NoIdListMapContainer.class, context); @Test // DATAJDBC-112 public void newEntityGetsConvertedToOneInsert() { @@ -419,6 +434,52 @@ public class RelationalEntityWriterUnitTests { ); } + @Test // DATAJDBC-223 + public void multiLevelQualifiedReferencesWithId() { + + ListMapContainer listMapContainer = new ListMapContainer(SOME_ENTITY_ID); + listMapContainer.maps.add(new MapContainer(SOME_ENTITY_ID)); + listMapContainer.maps.get(0).elements.put("one", new Element(null)); + + AggregateChange aggregateChange = new AggregateChange<>(Kind.SAVE, ListMapContainer.class, + listMapContainer); + + converter.write(listMapContainer, aggregateChange); + + assertThat(aggregateChange.getActions()) // + .extracting(DbAction::getClass, DbAction::getEntityType, a -> getQualifier(a, listMapContainerMaps),a -> getQualifier(a, listMapContainerElements), DbActionTestSupport::extractPath) // + .containsExactly( // + tuple(Delete.class, Element.class, null, null, "maps.elements"), // + tuple(Delete.class, MapContainer.class, null, null, "maps"), // + tuple(UpdateRoot.class, ListMapContainer.class, null, null, ""), // + tuple(Insert.class, MapContainer.class, 0, null, "maps"), // + tuple(Insert.class, Element.class, null, "one", "maps.elements") // + ); + } + + @Test // DATAJDBC-223 + public void multiLevelQualifiedReferencesWithOutId() { + + NoIdListMapContainer listMapContainer = new NoIdListMapContainer(SOME_ENTITY_ID); + listMapContainer.maps.add(new NoIdMapContainer()); + listMapContainer.maps.get(0).elements.put("one", new NoIdElement()); + + AggregateChange aggregateChange = new AggregateChange<>(Kind.SAVE, NoIdListMapContainer.class, + listMapContainer); + + converter.write(listMapContainer, aggregateChange); + + assertThat(aggregateChange.getActions()) // + .extracting(DbAction::getClass, DbAction::getEntityType, a -> getQualifier(a, noIdListMapContainerMaps),a -> getQualifier(a, noIdListMapContainerElements), DbActionTestSupport::extractPath) // + .containsExactly( // + tuple(Delete.class, NoIdElement.class, null, null, "maps.elements"), // + tuple(Delete.class, NoIdMapContainer.class, null, null, "maps"), // + tuple(UpdateRoot.class, NoIdListMapContainer.class, null, null, ""), // + tuple(Insert.class, NoIdMapContainer.class, 0, null, "maps"), // + tuple(Insert.class, NoIdElement.class, 0, "one", "maps.elements") // + ); + } + private CascadingReferenceMiddleElement createMiddleElement(Element first, Element second) { CascadingReferenceMiddleElement middleElement1 = new CascadingReferenceMiddleElement(null); @@ -429,21 +490,35 @@ public class RelationalEntityWriterUnitTests { private Object getMapKey(DbAction a) { - return a instanceof DbAction.WithDependingOn // - ? ((DbAction.WithDependingOn) a).getQualifiers().get(mapContainerElements) // - : null; + PersistentPropertyPath path = this.mapContainerElements; + + return getQualifier(a, path); } private Object getListKey(DbAction a) { + PersistentPropertyPath path = this.listContainerElements; + + return getQualifier(a, path); + } + + @Nullable + private Object getQualifier(DbAction a, PersistentPropertyPath path) { + return a instanceof DbAction.WithDependingOn // - ? ((DbAction.WithDependingOn) a).getQualifiers() - .get(listContainerElements) // + ? ((DbAction.WithDependingOn) a).getQualifiers().get(path) // : null; } + private int getQualifierCount(DbAction a, PersistentPropertyPath path) { + + return a instanceof DbAction.WithDependingOn // + ? ((DbAction.WithDependingOn) a).getQualifiers().size() // + : 0; + } + static PersistentPropertyPath toPath(String path, Class source, - RelationalMappingContext context) { + RelationalMappingContext context) { PersistentPropertyPaths persistentPropertyPaths = context .findPersistentPropertyPaths(source, p -> true); @@ -456,7 +531,7 @@ public class RelationalEntityWriterUnitTests { @Id final Long id; Element other; - // should not trigger own Dbaction + // should not trigger own DbAction String name; } @@ -472,7 +547,7 @@ public class RelationalEntityWriterUnitTests { @Id final Long id; NoIdElement other; - // should not trigger own Dbaction + // should not trigger own DbAction String name; } @@ -497,6 +572,13 @@ public class RelationalEntityWriterUnitTests { Set elements = new HashSet<>(); } + @RequiredArgsConstructor + private static class ListMapContainer { + + @Id final Long id; + List maps = new ArrayList<>(); + } + @RequiredArgsConstructor private static class MapContainer { @@ -516,6 +598,20 @@ public class RelationalEntityWriterUnitTests { @Id final Long id; } + + @RequiredArgsConstructor + private static class NoIdListMapContainer { + + @Id final Long id; + List maps = new ArrayList<>(); + } + + @RequiredArgsConstructor + private static class NoIdMapContainer { + + Map elements = new HashMap<>(); + } + @RequiredArgsConstructor private static class NoIdElement { // empty classes feel weird.