From 097190ee421ef43ca80a785dd8bb0295f868e53e Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 8 May 2019 14:02:44 +0200 Subject: [PATCH] DATAJDBC-223 - Chains of Lists or Maps get properly stored and loaded. Also solved DATAJDBC-369 since it made refactoring easier. Known limitation: Back-references to intermediate key columns currently can't get renamed and use the name defined by the property where they become part of the path. Backward compatibility of the JdbcConfiguration is still broken. Original pull request: #153. --- .../data/jdbc/core/EntityRowMapper.java | 2 +- .../data/jdbc/core/JdbcIdentifierBuilder.java | 16 +- .../jdbc/core/convert/BasicJdbcConverter.java | 164 +++++----- .../convert/CascadingDataAccessStrategy.java | 9 + .../jdbc/core/convert/DataAccessStrategy.java | 16 +- .../convert/DefaultDataAccessStrategy.java | 88 +++--- .../convert/DelegatingDataAccessStrategy.java | 9 + .../jdbc/core/convert/EntityRowMapper.java | 24 +- .../data/jdbc/core/convert/JdbcConverter.java | 10 +- .../jdbc/core/convert/MapEntityRowMapper.java | 25 +- .../jdbc/core/convert/RelationResolver.java | 41 +++ .../data/jdbc/core/convert/SqlGenerator.java | 42 ++- .../mybatis/MyBatisDataAccessStrategy.java | 11 +- .../config/AbstractJdbcConfiguration.java | 34 ++- .../repository/config/JdbcConfiguration.java | 27 +- .../support/JdbcQueryLookupStrategy.java | 5 +- .../core/DefaultJdbcInterpreterUnitTests.java | 56 +++- ...JdbcAggregateTemplateIntegrationTests.java | 282 +++++++++++++++++- ...lConverterAggregateReferenceUnitTests.java | 6 +- .../DefaultDataAccessStrategyUnitTests.java | 38 ++- .../convert/EntityRowMapperUnitTests.java | 50 +++- .../core/convert/SqlGeneratorUnitTests.java | 35 ++- .../SimpleJdbcRepositoryEventsUnitTests.java | 5 +- .../JdbcRepositoryFactoryBeanUnitTests.java | 4 +- .../data/jdbc/testing/TestConfiguration.java | 22 +- ...AggregateTemplateIntegrationTests-hsql.sql | 184 +++++++++++- ...regateTemplateIntegrationTests-mariadb.sql | 168 +++++++++++ ...ggregateTemplateIntegrationTests-mssql.sql | 167 +++++++++++ ...ggregateTemplateIntegrationTests-mysql.sql | 167 +++++++++++ ...egateTemplateIntegrationTests-postgres.sql | 168 +++++++++++ .../relational/core/conversion/PathNode.java | 22 +- .../conversion/RelationalPropertyPath.java | 72 ----- .../core/conversion/WritingContext.java | 21 +- .../PersistentPropertyPathExtension.java | 160 +++++++--- .../RelationalEntityWriterUnitTests.java | 116 ++++++- 35 files changed, 1908 insertions(+), 358 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RelationResolver.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalPropertyPath.java 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.