From b1c68d901eba5e203351b44fcb307bddc777ca42 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 10 Apr 2019 16:55:40 +0200 Subject: [PATCH] DATAJDBC-359 - Support for arbitrary chains of entities with or without Id. Introduced a `ReadingContext` in the `EntityRowMapper` to avoid passing the `ResultSet` and the `path` all over the place. Added a dependency test to Spring Data Relational and fixed the test failure by moving the PersistentPropertyPathExtension to core.mapping. Original pull request: #150. --- .../jdbc/core/DefaultDataAccessStrategy.java | 470 +---------------- .../jdbc/core/DefaultJdbcInterpreter.java | 61 ++- .../data/jdbc/core/EntityRowMapper.java | 207 +------- .../{convert => }/JdbcIdentifierBuilder.java | 44 +- .../jdbc/core/convert/BasicJdbcConverter.java | 204 +++++++- .../convert/DefaultDataAccessStrategy.java | 483 ++++++++++++++++++ .../jdbc/core/convert/EntityRowMapper.java | 59 +++ .../IterableOfEntryToMapConverter.java | 2 +- .../data/jdbc/core/convert/JdbcConverter.java | 10 + .../{ => convert}/MapEntityRowMapper.java | 2 +- .../jdbc/core/{ => convert}/SqlContext.java | 3 +- .../jdbc/core/{ => convert}/SqlGenerator.java | 96 ++-- .../{ => convert}/SqlGeneratorSource.java | 2 +- .../mybatis/MyBatisDataAccessStrategy.java | 7 +- .../config/AbstractJdbcConfiguration.java | 9 +- .../repository/config/JdbcConfiguration.java | 4 +- .../support/JdbcQueryLookupStrategy.java | 12 +- .../support/JdbcRepositoryFactory.java | 9 +- .../support/JdbcRepositoryFactoryBean.java | 4 +- .../core/DefaultJdbcInterpreterUnitTests.java | 44 +- ...JdbcAggregateTemplateIntegrationTests.java | 73 ++- ...sistentPropertyPathExtensionUnitTests.java | 147 ++++-- .../jdbc/core/PropertyPathTestingUtils.java | 6 +- .../DefaultDataAccessStrategyUnitTests.java | 3 +- .../EntityRowMapperUnitTests.java | 210 ++++++-- ...terableOfEntryToMapConverterUnitTests.java | 2 +- .../JdbcIdentifierBuilderUnitTests.java | 40 +- ...orContextBasedNamingStrategyUnitTests.java | 3 +- .../SqlGeneratorEmbeddedUnitTests.java | 6 +- ...GeneratorFixedNamingStrategyUnitTests.java | 3 +- .../{ => convert}/SqlGeneratorUnitTests.java | 65 ++- .../data/jdbc/degraph/DependencyTests.java | 8 +- ...EmbeddedWithReferenceIntegrationTests.java | 1 + .../SimpleJdbcRepositoryEventsUnitTests.java | 5 +- ...nableJdbcRepositoriesIntegrationTests.java | 27 +- .../JdbcQueryLookupStrategyUnitTests.java | 4 +- .../JdbcRepositoryFactoryBeanUnitTests.java | 2 +- .../data/jdbc/testing/TestConfiguration.java | 10 +- ...AggregateTemplateIntegrationTests-hsql.sql | 34 ++ ...regateTemplateIntegrationTests-mariadb.sql | 34 ++ ...ggregateTemplateIntegrationTests-mssql.sql | 36 +- ...ggregateTemplateIntegrationTests-mysql.sql | 36 +- ...egateTemplateIntegrationTests-postgres.sql | 36 +- ...dedWithCollectionIntegrationTests-hsql.sql | 15 +- ...ddedWithReferenceIntegrationTests-hsql.sql | 13 +- .../core/conversion/AggregateChange.java | 106 ++-- .../BasicRelationalPersistentProperty.java | 6 + .../core/mapping/NamingStrategy.java | 5 + .../PersistentPropertyPathExtension.java | 92 +++- .../mapping/RelationalPersistentProperty.java | 9 + .../data/relational/domain/package-info.java | 4 + .../conversion/AggregateChangeUnitTests.java | 8 +- .../relational/degraph/DependencyTests.java | 65 +++ 53 files changed, 1857 insertions(+), 989 deletions(-) rename spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/{convert => }/JdbcIdentifierBuilder.java (68%) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java rename spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/{ => convert}/IterableOfEntryToMapConverter.java (97%) rename spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/{ => convert}/MapEntityRowMapper.java (97%) rename spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/{ => convert}/SqlContext.java (92%) rename spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/{ => convert}/SqlGenerator.java (92%) rename spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/{ => convert}/SqlGeneratorSource.java (96%) rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/{ => convert}/DefaultDataAccessStrategyUnitTests.java (97%) rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/{ => convert}/EntityRowMapperUnitTests.java (73%) rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/{ => convert}/IterableOfEntryToMapConverterUnitTests.java (97%) rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/{ => convert}/JdbcIdentifierBuilderUnitTests.java (75%) rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/{ => convert}/SqlGeneratorContextBasedNamingStrategyUnitTests.java (98%) rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/{ => convert}/SqlGeneratorEmbeddedUnitTests.java (97%) rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/{ => convert}/SqlGeneratorFixedNamingStrategyUnitTests.java (98%) rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/{ => convert}/SqlGeneratorUnitTests.java (92%) rename {spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core => spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping}/PersistentPropertyPathExtension.java (76%) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/domain/package-info.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/degraph/DependencyTests.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java index dbd32ce8..5154c3f8 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * 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. @@ -15,470 +15,32 @@ */ package org.springframework.data.jdbc.core; -import lombok.NonNull; - -import java.sql.JDBCType; -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.function.Predicate; - -import org.springframework.dao.DataRetrievalFailureException; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.jdbc.core.convert.JdbcConverter; -import org.springframework.data.jdbc.core.convert.JdbcValue; -import org.springframework.data.jdbc.support.JdbcUtil; -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.jdbc.core.convert.SqlGeneratorSource; 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.jdbc.core.RowMapper; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** * The default {@link DataAccessStrategy} is to generate SQL statements based on meta data from the entity. - * + * * @author Jens Schauder - * @author Mark Paluch - * @author Thomas Lang - * @author Bastian Wilhelm + * @deprecated Use {@link org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy} instead. */ -public class DefaultDataAccessStrategy implements DataAccessStrategy { - - private final @NonNull SqlGeneratorSource sqlGeneratorSource; - private final @NonNull RelationalMappingContext context; - private final @NonNull JdbcConverter converter; - private final @NonNull NamedParameterJdbcOperations operations; - private final @NonNull DataAccessStrategy accessStrategy; +@Deprecated +public class DefaultDataAccessStrategy extends org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy { /** - * 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} - * - * @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}. - * @param mappingAccessStrategy can be {@literal null}. - * @since 1.1 - */ - public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, RelationalMappingContext context, - JdbcConverter converter, NamedParameterJdbcOperations operations, - @Nullable DataAccessStrategy mappingAccessStrategy) { - - Assert.notNull(sqlGeneratorSource, "SqlGeneratorSource must not be null"); - Assert.notNull(context, "RelationalMappingContext must not be null"); - Assert.notNull(converter, "JdbcConverter must not be null"); - Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); - - this.sqlGeneratorSource = sqlGeneratorSource; - this.context = context; - this.converter = converter; - this.operations = operations; - this.accessStrategy = mappingAccessStrategy == null ? this : mappingAccessStrategy; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) - */ - @Override - public Object insert(T instance, Class domainType, Map additionalParameters) { - return insert(instance, domainType, Identifier.from(additionalParameters)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) - */ - @Override - public Object insert(T instance, Class domainType, Identifier identifier) { - - KeyHolder holder = new GeneratedKeyHolder(); - RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - - MapSqlParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", - PersistentProperty::isIdProperty); - - identifier.forEach((name, value, type) -> addConvertedPropertyValue(parameterSource, name, value, type)); - - Object idValue = getIdValueOrNull(instance, persistentEntity); - if (idValue != null) { - - RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); - addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); - } - - operations.update( // - sql(domainType).getInsert(new HashSet<>(Arrays.asList(parameterSource.getParameterNames()))), // - parameterSource, // - holder // - ); - - return getIdFromHolder(holder, persistentEntity); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#update(java.lang.Object, java.lang.Class) - */ - @Override - public boolean update(S instance, Class domainType) { - - RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - - return operations.update(sql(domainType).getUpdate(), - getParameterSource(instance, persistentEntity, "", Predicates.includeAll())) != 0; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, java.lang.Class) - */ - @Override - public void delete(Object id, Class domainType) { - - String deleteByIdSql = sql(domainType).getDeleteById(); - MapSqlParameterSource parameter = createIdParameterSource(id, domainType); - - operations.update(deleteByIdSql, parameter); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PropertyPath) - */ - @Override - public void delete(Object rootId, PersistentPropertyPath propertyPath) { - - RelationalPersistentEntity rootEntity = context - .getRequiredPersistentEntity(propertyPath.getBaseProperty().getOwner().getType()); - - RelationalPersistentProperty referencingProperty = propertyPath.getLeafProperty(); - Assert.notNull(referencingProperty, "No property found matching the PropertyPath " + propertyPath); - - String format = sql(rootEntity.getType()).createDeleteByPath(propertyPath); - - HashMap parameters = new HashMap<>(); - parameters.put("rootId", rootId); - operations.update(format, parameters); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteAll(java.lang.Class) - */ - @Override - public void deleteAll(Class domainType) { - operations.getJdbcOperations().update(sql(domainType).createDeleteAllSql(null)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteAll(org.springframework.data.mapping.PropertyPath) - */ - @Override - public void deleteAll(PersistentPropertyPath propertyPath) { - operations.getJdbcOperations() - .update(sql(propertyPath.getBaseProperty().getOwner().getType()).createDeleteAllSql(propertyPath)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class) - */ - @Override - public long count(Class domainType) { - - Long result = operations.getJdbcOperations().queryForObject(sql(domainType).getCount(), Long.class); - - Assert.notNull(result, "The result of a count query must not be null."); - - return result; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#findById(java.lang.Object, java.lang.Class) - */ - @SuppressWarnings("unchecked") - @Override - public T findById(Object id, Class domainType) { - - String findOneSql = sql(domainType).getFindOne(); - MapSqlParameterSource parameter = createIdParameterSource(id, domainType); - - try { - return operations.queryForObject(findOneSql, parameter, (RowMapper) getEntityRowMapper(domainType)); - } catch (EmptyResultDataAccessException e) { - return null; - } - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAll(java.lang.Class) - */ - @SuppressWarnings("unchecked") - @Override - public Iterable findAll(Class domainType) { - return operations.query(sql(domainType).getFindAll(), (RowMapper) getEntityRowMapper(domainType)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllById(java.lang.Iterable, java.lang.Class) - */ - @SuppressWarnings("unchecked") - @Override - public Iterable findAllById(Iterable ids, Class domainType) { - - RelationalPersistentProperty idProperty = getRequiredPersistentEntity(domainType).getRequiredIdProperty(); - MapSqlParameterSource parameterSource = new MapSqlParameterSource(); - - addConvertedPropertyValuesAsList(parameterSource, idProperty, ids, "ids"); - - String findAllInListSql = sql(domainType).getFindAllInList(); - - return operations.query(findAllInListSql, parameterSource, (RowMapper) getEntityRowMapper(domainType)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllByProperty(java.lang.Object, org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty) - */ - @Override - @SuppressWarnings("unchecked") - public Iterable findAllByProperty(Object rootId, RelationalPersistentProperty property) { - - 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))); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#existsById(java.lang.Object, java.lang.Class) - */ - @Override - public boolean existsById(Object id, Class domainType) { - - String existsSql = sql(domainType).getExists(); - MapSqlParameterSource parameter = createIdParameterSource(id, domainType); - - Boolean result = operations.queryForObject(existsSql, parameter, Boolean.class); - - Assert.notNull(result, "The result of an exists query must not be null"); - - return result; - } - - private MapSqlParameterSource getParameterSource(S instance, RelationalPersistentEntity persistentEntity, - String prefix, Predicate skipProperty) { - - MapSqlParameterSource parameters = new MapSqlParameterSource(); - - PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(instance); - - persistentEntity.doWithProperties((PropertyHandler) property -> { - - if (skipProperty.test(property)) { - return; - } - if (property.isEntity() && !property.isEmbedded()) { - return; - } - - if (property.isEmbedded()) { - - Object value = propertyAccessor.getProperty(property); - RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getType()); - MapSqlParameterSource additionalParameters = getParameterSource((T) value, - (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty); - parameters.addValues(additionalParameters.getValues()); - } else { - - Object value = propertyAccessor.getProperty(property); - String paramName = prefix + property.getColumnName(); - - addConvertedPropertyValue(parameters, property, value, paramName); - } - }); - - return parameters; - } - - @SuppressWarnings("unchecked") - @Nullable - private ID getIdValueOrNull(S instance, RelationalPersistentEntity persistentEntity) { - - ID idValue = (ID) persistentEntity.getIdentifierAccessor(instance).getIdentifier(); - - return isIdPropertyNullOrScalarZero(idValue, persistentEntity) ? null : idValue; - } - - private static boolean isIdPropertyNullOrScalarZero(@Nullable ID idValue, - RelationalPersistentEntity persistentEntity) { - - RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); - return idValue == null // - || idProperty == null // - || (idProperty.getType() == int.class && idValue.equals(0)) // - || (idProperty.getType() == long.class && idValue.equals(0L)); - } - - @Nullable - private Object getIdFromHolder(KeyHolder holder, RelationalPersistentEntity persistentEntity) { - - try { - // MySQL just returns one value with a special name - return holder.getKey(); - } catch (DataRetrievalFailureException | InvalidDataAccessApiUsageException e) { - // Postgres returns a value for each column - // MS SQL Server returns a value that might be null. - - Map keys = holder.getKeys(); - - if (keys == null || persistentEntity.getIdProperty() == null) { - return null; - } - - return keys.get(persistentEntity.getIdColumn()); - } - } - - private EntityRowMapper getEntityRowMapper(Class domainType) { - return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context, converter, accessStrategy); - } - - private RowMapper getMapEntityRowMapper(RelationalPersistentProperty property) { - - String keyColumn = property.getKeyColumn(); - Assert.notNull(keyColumn, () -> "KeyColumn must not be null for " + property); - - return new MapEntityRowMapper<>(getEntityRowMapper(property.getActualType()), keyColumn); - } - - private MapSqlParameterSource createIdParameterSource(Object id, Class domainType) { - - MapSqlParameterSource parameterSource = new MapSqlParameterSource(); - - addConvertedPropertyValue( // - parameterSource, // - getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // - id, // - "id" // - ); - return parameterSource; - } - - private void addConvertedPropertyValue(MapSqlParameterSource parameterSource, RelationalPersistentProperty property, - Object value, String paramName) { - - JdbcValue jdbcValue = converter.writeJdbcValue( // - value, // - property.getColumnType(), // - property.getSqlType() // - ); - - parameterSource.addValue(paramName, jdbcValue.getValue(), JdbcUtil.sqlTypeFor(jdbcValue.getJdbcType())); - } - - private void addConvertedPropertyValue(MapSqlParameterSource parameterSource, String name, Object value, - Class type) { - - JdbcValue jdbcValue = converter.writeJdbcValue( // - value, // - type, // - JdbcUtil.sqlTypeFor(type) // - ); - - parameterSource.addValue( // - name, // - jdbcValue.getValue(), // - JdbcUtil.sqlTypeFor(jdbcValue.getJdbcType()) // - ); - } - - private void addConvertedPropertyValuesAsList(MapSqlParameterSource parameterSource, - RelationalPersistentProperty property, Iterable values, String paramName) { - - List convertedIds = new ArrayList<>(); - JdbcValue jdbcValue = null; - for (Object id : values) { - - Class columnType = property.getColumnType(); - int sqlType = property.getSqlType(); - - jdbcValue = converter.writeJdbcValue(id, columnType, sqlType); - convertedIds.add(jdbcValue.getValue()); - } - - Assert.notNull(jdbcValue, "JdbcValue must be not null at this point. Please report this as a bug."); - - JDBCType jdbcType = jdbcValue.getJdbcType(); - int typeNumber = jdbcType == null ? JdbcUtils.TYPE_UNKNOWN : jdbcType.getVendorTypeNumber(); - - parameterSource.addValue(paramName, convertedIds, typeNumber); - } - - @SuppressWarnings("unchecked") - private RelationalPersistentEntity getRequiredPersistentEntity(Class domainType) { - return (RelationalPersistentEntity) context.getRequiredPersistentEntity(domainType); - } - - private SqlGenerator sql(Class domainType) { - return sqlGeneratorSource.getSqlGenerator(domainType); - } - - /** - * Utility to create {@link Predicate}s. - */ - static class Predicates { - - /** - * Include all {@link Predicate} returning {@literal false} to never skip a property. + * Creates a {@link org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy} which references it self for resolution of recursive data accesses. + * Only suitable if this is the only access strategy in use. * - * @return the include all {@link Predicate}. + * @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}. */ - static Predicate includeAll() { - return it -> false; + public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, RelationalMappingContext context, + JdbcConverter converter, NamedParameterJdbcOperations operations) { + super(sqlGeneratorSource, context, converter, operations); } - } + } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java index cf3ffaf7..23293994 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java @@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor; import java.util.Collections; import java.util.Map; -import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.conversion.DbAction; import org.springframework.data.relational.core.conversion.DbAction.Delete; @@ -33,11 +32,12 @@ import org.springframework.data.relational.core.conversion.DbAction.Merge; import org.springframework.data.relational.core.conversion.DbAction.Update; import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot; import org.springframework.data.relational.core.conversion.Interpreter; +import org.springframework.data.relational.core.mapping.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; import org.springframework.data.relational.domain.Identifier; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * {@link Interpreter} for {@link DbAction}s using a {@link DataAccessStrategy} for performing actual database @@ -144,38 +144,67 @@ class DefaultJdbcInterpreter implements Interpreter { private Identifier getParentKeys(DbAction.WithDependingOn action) { - DbAction.WithEntity dependingOn = action.getDependingOn(); + Object id = getParentId(action); - RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(dependingOn.getEntityType()); - - Object id = getIdFromEntityDependingOn(dependingOn, persistentEntity); JdbcIdentifierBuilder identifier = JdbcIdentifierBuilder // - .forBackReferences(action.getPropertyPath(), id); + .forBackReferences(new PersistentPropertyPathExtension(context, action.getPropertyPath()), id); for (Map.Entry, Object> qualifier : action.getQualifiers() .entrySet()) { - identifier = identifier.withQualifier(qualifier.getKey(), qualifier.getValue()); + identifier = identifier.withQualifier(new PersistentPropertyPathExtension(context, qualifier.getKey()), + qualifier.getValue()); } return identifier.build(); } - @Nullable - private Object getIdFromEntityDependingOn(DbAction.WithEntity dependingOn, - RelationalPersistentEntity persistentEntity) { + private Object getParentId(DbAction.WithDependingOn action) { - Object entity = dependingOn.getEntity(); + PersistentPropertyPathExtension path = new PersistentPropertyPathExtension(context, action.getPropertyPath()); + PersistentPropertyPathExtension idPath = path.getIdDefiningParentPath(); - if (dependingOn instanceof DbAction.WithGeneratedId) { + DbAction.WithEntity idOwningAction = getIdOwningAction(action, idPath); - Object generatedId = ((DbAction.WithGeneratedId) dependingOn).getGeneratedId(); + return getIdFrom(idOwningAction); + } + + @SuppressWarnings("unchecked") + private DbAction.WithEntity getIdOwningAction(DbAction.WithEntity action, PersistentPropertyPathExtension idPath) { + + if (!(action instanceof DbAction.WithDependingOn)) { + + Assert.state(idPath.getLength() == 0, + "When the id path is not empty the id providing action should be of type WithDependingOn"); + + return action; + } + + DbAction.WithDependingOn withDependingOn = (DbAction.WithDependingOn) action; + + if (idPath.matches(withDependingOn.getPropertyPath())) { + return action; + } + + return getIdOwningAction(withDependingOn.getDependingOn(), idPath); + } + + private Object getIdFrom(DbAction.WithEntity idOwningAction) { + + if (idOwningAction instanceof DbAction.WithGeneratedId) { + + Object generatedId = ((DbAction.WithGeneratedId) idOwningAction).getGeneratedId(); if (generatedId != null) { return generatedId; } } - return persistentEntity.getIdentifierAccessor(entity).getIdentifier(); - } + RelationalPersistentEntity persistentEntity = context + .getRequiredPersistentEntity(idOwningAction.getEntityType()); + Object identifier = persistentEntity.getIdentifierAccessor(idOwningAction.getEntity()).getIdentifier(); + Assert.state(identifier != null, "Couldn't get obtain a required id value"); + + return identifier; + } } 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 6fce6456..1080486b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * 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. @@ -15,209 +15,20 @@ */ package org.springframework.data.jdbc.core; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Map; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.data.mapping.MappingException; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.relational.core.conversion.RelationalConverter; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** - * Maps a {@link ResultSet} to an entity of type {@code T}, including entities referenced. This {@link RowMapper} might - * trigger additional SQL statements in order to load other members of the same aggregate. - * * @author Jens Schauder - * @author Oliver Gierke - * @author Mark Paluch - * @author Maciej Walkowiak - * @author Bastian Wilhelm + * + * @deprecated Use {@link org.springframework.data.jdbc.core.convert.EntityRowMapper} instead. */ -public class EntityRowMapper implements RowMapper { +@Deprecated +public class EntityRowMapper extends org.springframework.data.jdbc.core.convert.EntityRowMapper { - private static final Converter, Map> ITERABLE_OF_ENTRY_TO_MAP_CONVERTER = new IterableOfEntryToMapConverter(); - - private final RelationalPersistentEntity entity; - - private final RelationalConverter converter; - private final RelationalMappingContext context; - private final DataAccessStrategy accessStrategy; - private final RelationalPersistentProperty idProperty; - - public EntityRowMapper(RelationalPersistentEntity entity, RelationalMappingContext context, - RelationalConverter converter, DataAccessStrategy accessStrategy) { - - this.entity = entity; - this.converter = converter; - this.context = context; - this.accessStrategy = accessStrategy; - this.idProperty = entity.getIdProperty(); + public EntityRowMapper(RelationalPersistentEntity entity, JdbcConverter converter, + DataAccessStrategy accessStrategy) { + super(entity, converter, accessStrategy); } - /* - * (non-Javadoc) - * @see org.springframework.jdbc.core.RowMapper#mapRow(java.sql.ResultSet, int) - */ - @Override - public T mapRow(ResultSet resultSet, int rowNumber) { - - String prefix = ""; - - RelationalPersistentProperty idProperty = entity.getIdProperty(); - - Object idValue = null; - if (idProperty != null) { - idValue = readFrom(resultSet, idProperty, prefix); - } - - T result = createInstance(entity, resultSet, idValue, prefix); - - return entity.requiresPropertyPopulation() // - ? populateProperties(result, resultSet) // - : result; - } - - private T populateProperties(T result, ResultSet resultSet) { - - PersistentPropertyAccessor propertyAccessor = converter.getPropertyAccessor(entity, result); - - Object id = idProperty == null ? null : readFrom(resultSet, idProperty, ""); - - PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); - - for (RelationalPersistentProperty property : entity) { - - if (persistenceConstructor != null && persistenceConstructor.isConstructorParameter(property)) { - continue; - } - - propertyAccessor.setProperty(property, readOrLoadProperty(resultSet, id, property, "")); - } - - return propertyAccessor.getBean(); - } - - @Nullable - private Object readOrLoadProperty(ResultSet resultSet, @Nullable Object id, RelationalPersistentProperty property, - String prefix) { - - 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)); - } else if (property.isEmbedded()) { - return readEmbeddedEntityFrom(resultSet, id, property, prefix); - } else { - return readFrom(resultSet, property, prefix); - } - } - - /** - * Read a single value or a complete Entity from the {@link ResultSet} passed as an argument. - * - * @param resultSet the {@link ResultSet} to extract the value from. Must not be {@code null}. - * @param property the {@link RelationalPersistentProperty} for which the value is intended. Must not be {@code null}. - * @param prefix to be used for all column names accessed by this method. Must not be {@code null}. - * @return the value read from the {@link ResultSet}. May be {@code null}. - */ - @Nullable - private Object readFrom(ResultSet resultSet, RelationalPersistentProperty property, String prefix) { - - if (property.isEntity()) { - return readEntityFrom(resultSet, property, prefix); - } - - Object value = getObjectFromResultSet(resultSet, prefix + property.getColumnName()); - return converter.readValue(value, property.getTypeInformation()); - - } - - private Object readEmbeddedEntityFrom(ResultSet rs, @Nullable Object id, RelationalPersistentProperty property, - String prefix) { - - String newPrefix = prefix + property.getEmbeddedPrefix(); - - RelationalPersistentEntity entity = context.getRequiredPersistentEntity(property.getActualType()); - - Object instance = createInstance(entity, rs, null, newPrefix); - - @SuppressWarnings("unchecked") - PersistentPropertyAccessor accessor = converter.getPropertyAccessor((PersistentEntity) entity, - instance); - - for (RelationalPersistentProperty p : entity) { - accessor.setProperty(p, readOrLoadProperty(rs, id, p, newPrefix)); - } - - return instance; - } - - @Nullable - private S readEntityFrom(ResultSet rs, RelationalPersistentProperty property, String prefix) { - - String newPrefix = prefix + property.getName() + "_"; - - @SuppressWarnings("unchecked") - RelationalPersistentEntity entity = (RelationalPersistentEntity) context - .getRequiredPersistentEntity(property.getActualType()); - - RelationalPersistentProperty idProperty = entity.getIdProperty(); - - Object idValue = null; - - if (idProperty != null) { - idValue = readFrom(rs, idProperty, newPrefix); - } - - if ((idProperty != null // - ? idValue // - : getObjectFromResultSet(rs, newPrefix + property.getReverseColumnName()) // - ) == null) { - return null; - } - - S instance = createInstance(entity, rs, idValue, newPrefix); - - PersistentPropertyAccessor accessor = converter.getPropertyAccessor(entity, instance); - - for (RelationalPersistentProperty p : entity) { - accessor.setProperty(p, readOrLoadProperty(rs, idValue, p, newPrefix)); - } - - return instance; - } - - @Nullable - private Object getObjectFromResultSet(ResultSet rs, String backreferenceName) { - - try { - return rs.getObject(backreferenceName); - } catch (SQLException o_O) { - throw new MappingException(String.format("Could not read value %s from result set!", backreferenceName), o_O); - } - } - - private S createInstance(RelationalPersistentEntity entity, ResultSet rs, @Nullable Object idValue, - String prefix) { - - return converter.createInstance(entity, parameter -> { - - String parameterName = parameter.getName(); - - Assert.notNull(parameterName, "A constructor parameter name must not be null to be used with Spring Data JDBC"); - - RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameterName); - - return readOrLoadProperty(rs, idValue, property, prefix); - }); - } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilder.java similarity index 68% rename from spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilder.java index c3f027d1..87dc0406 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilder.java @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.convert; +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; /** * Builder for {@link Identifier}. Mainly for internal use within the framework @@ -41,30 +43,17 @@ public class JdbcIdentifierBuilder { /** * Creates ParentKeys with backreference for the given path and value of the parents id. */ - public static JdbcIdentifierBuilder forBackReferences(PersistentPropertyPath path, - @Nullable Object value) { + public static JdbcIdentifierBuilder forBackReferences(PersistentPropertyPathExtension path, @Nullable Object value) { Identifier identifier = Identifier.of( // - path.getRequiredLeafProperty().getReverseColumnName(), // + path.getReverseColumnName(), // value, // - getLastIdProperty(path).getColumnType() // + path.getIdDefiningParentPath().getRequiredIdProperty().getColumnType() // ); return new JdbcIdentifierBuilder(identifier); } - public JdbcIdentifierBuilder withQualifier(PersistentPropertyPath path, Object value) { - - RelationalPersistentProperty leafProperty = path.getRequiredLeafProperty(); - identifier = identifier.withPart(leafProperty.getKeyColumn(), value, leafProperty.getQualifierColumnType()); - - return this; - } - - public Identifier build() { - return identifier; - } - private static RelationalPersistentProperty getLastIdProperty( PersistentPropertyPath path) { @@ -76,4 +65,25 @@ public class JdbcIdentifierBuilder { return getLastIdProperty(path.getParentPath()); } + + /** + * Adds a qualifier to the identifier to build. A qualifier is a map key or a list index. + * + * @param path path to the map that gets qualified by {@code value}. Must not be {@literal null}. + * @param value map key or list index qualifying the map identified by {@code path}. Must not be {@literal null}. + * @return this builder. Guaranteed to be not {@literal null}. + */ + public JdbcIdentifierBuilder withQualifier(PersistentPropertyPathExtension path, Object value) { + + Assert.notNull(path, "Path must not be null"); + Assert.notNull(value, "Value must not be null"); + + identifier = identifier.withPart(path.getKeyColumn(), value, path.getQualifierColumnType()); + + return this; + } + + public Identifier build() { + return identifier; + } } 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 0d48026a..42cdebf2 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 @@ -15,22 +15,32 @@ */ package org.springframework.data.jdbc.core.convert; +import lombok.Value; + import java.sql.Array; import java.sql.JDBCType; +import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Map; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.CustomConversions; +import org.springframework.data.jdbc.core.DataAccessStrategy; 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.PersistentEntity; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; 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.util.ClassTypeInformation; @@ -53,7 +63,7 @@ import org.springframework.util.Assert; public class BasicJdbcConverter extends BasicRelationalConverter implements JdbcConverter { 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; /** @@ -238,4 +248,194 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc return null; } + + /* + * (non-Javadoc) + * @see org.springframework.jdbc.core.RowMapper#mapRow(java.sql.ResultSet, int) + */ + @Override + public T mapRow(RelationalPersistentEntity entity, DataAccessStrategy accessStrategy, ResultSet resultSet) { + return new ReadingContext(entity, accessStrategy, resultSet).mapRow(); + } + + @Value + private class ReadingContext { + + private final RelationalPersistentEntity entity; + private final RelationalPersistentProperty idProperty; + + private final ResultSet resultSet; + PersistentPropertyPathExtension path; + private final DataAccessStrategy accessStrategy; + + ReadingContext(RelationalPersistentEntity entity, DataAccessStrategy accessStrategy, ResultSet resultSet) { + + this.entity = entity; + this.idProperty = entity.getIdProperty(); + this.accessStrategy = accessStrategy; + this.resultSet = resultSet; + this.path = new PersistentPropertyPathExtension((MappingContext, RelationalPersistentProperty>) getMappingContext(), entity); + } + + public ReadingContext(RelationalPersistentEntity entity, DataAccessStrategy accessStrategy, ResultSet resultSet, PersistentPropertyPathExtension path) { + + this.entity = entity; + this.idProperty = entity.getIdProperty(); + this.accessStrategy = accessStrategy; + this.resultSet = resultSet; + this.path = path; + } + + private ReadingContext extendBy(RelationalPersistentProperty property) { + return new ReadingContext(entity, accessStrategy, resultSet, path.extendBy(property)); + } + + T mapRow() { + + RelationalPersistentProperty idProperty = entity.getIdProperty(); + + Object idValue = null; + if (idProperty != null) { + idValue = readFrom(idProperty); + } + + T result = createInstanceInternal(entity, idValue); + + return entity.requiresPropertyPopulation() // + ? populateProperties(result) // + : result; + } + + private T populateProperties(T result) { + + PersistentPropertyAccessor propertyAccessor = getPropertyAccessor(entity, result); + + Object id = idProperty == null ? null : readFrom(idProperty); + + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + + for (RelationalPersistentProperty property : entity) { + + if (persistenceConstructor != null && persistenceConstructor.isConstructorParameter(property)) { + continue; + } + + propertyAccessor.setProperty(property, readOrLoadProperty(id, property)); + } + + return propertyAccessor.getBean(); + } + + @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)); + } else if (property.isEmbedded()) { + return readEmbeddedEntityFrom(id, property); + } else { + return readFrom(property); + } + } + + /** + * Read a single value or a complete Entity from the {@link ResultSet} passed as an argument. + * + * @param property the {@link RelationalPersistentProperty} for which the value is intended. Must not be + * {@code null}. + * @return the value read from the {@link ResultSet}. May be {@code null}. + */ + @Nullable + private Object readFrom(RelationalPersistentProperty property) { + + if (property.isEntity()) { + return readEntityFrom(property, path); + } + + Object value = getObjectFromResultSet(path.extendBy(property).getColumnAlias()); + return readValue(value, property.getTypeInformation()); + + } + + private Object readEmbeddedEntityFrom(@Nullable Object id, RelationalPersistentProperty property) { + + ReadingContext newContext = extendBy(property); + + RelationalPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(property.getActualType()); + + Object instance = newContext.createInstanceInternal(entity, null); + + @SuppressWarnings("unchecked") + PersistentPropertyAccessor accessor = getPropertyAccessor((PersistentEntity) entity, instance); + + for (RelationalPersistentProperty p : entity) { + accessor.setProperty(p, newContext.readOrLoadProperty(id, p)); + } + + return instance; + } + + @Nullable + private S readEntityFrom(RelationalPersistentProperty property, PersistentPropertyPathExtension path) { + + @SuppressWarnings("unchecked") + ReadingContext newContext = extendBy(property); + + @SuppressWarnings("unchecked") + RelationalPersistentEntity entity = (RelationalPersistentEntity) getMappingContext() + .getRequiredPersistentEntity(property.getActualType()); + + RelationalPersistentProperty idProperty = entity.getIdProperty(); + + Object idValue = null; + + if (idProperty != null) { + idValue = newContext.readFrom(idProperty); + } + + if ((idProperty != null // + ? idValue // + : newContext.getObjectFromResultSet(path.extendBy(property).getReverseColumnNameAlias()) // + ) == null) { + return null; + } + + S instance = newContext.createInstanceInternal(entity, idValue); + + PersistentPropertyAccessor accessor = getPropertyAccessor(entity, instance); + + for (RelationalPersistentProperty p : entity) { + accessor.setProperty(p, newContext.readOrLoadProperty(idValue, p)); + } + + return instance; + } + + @Nullable + private Object getObjectFromResultSet(String backreferenceName) { + + try { + return resultSet.getObject(backreferenceName); + } catch (SQLException o_O) { + throw new MappingException(String.format("Could not read value %s from result set!", backreferenceName), o_O); + } + } + + private S createInstanceInternal(RelationalPersistentEntity entity, @Nullable Object idValue) { + + return createInstance(entity,parameter -> { + + String parameterName = parameter.getName(); + + Assert.notNull(parameterName, "A constructor parameter name must not be null to be used with Spring Data JDBC"); + + RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameterName); + + return readOrLoadProperty(idValue, 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 new file mode 100644 index 00000000..e6f8e7d0 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -0,0 +1,483 @@ +/* + * 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.jdbc.core.convert; + +import lombok.NonNull; + +import java.sql.JDBCType; +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.function.Predicate; + +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.jdbc.core.DataAccessStrategy; +import org.springframework.data.jdbc.support.JdbcUtil; +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.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.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * The default {@link DataAccessStrategy} is to generate SQL statements based on meta data from the entity. + * + * @author Jens Schauder + * @author Mark Paluch + * @author Thomas Lang + * @author Bastian Wilhelm + */ +public class DefaultDataAccessStrategy implements DataAccessStrategy { + + private final @NonNull SqlGeneratorSource sqlGeneratorSource; + 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} + * + * @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}. + * @param mappingAccessStrategy can be {@literal null}. + * @since 1.1 + */ + public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, RelationalMappingContext context, + JdbcConverter converter, NamedParameterJdbcOperations operations, + @Nullable DataAccessStrategy mappingAccessStrategy) { + + Assert.notNull(sqlGeneratorSource, "SqlGeneratorSource must not be null"); + Assert.notNull(context, "RelationalMappingContext must not be null"); + Assert.notNull(converter, "JdbcConverter must not be null"); + Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); + + this.sqlGeneratorSource = sqlGeneratorSource; + this.context = context; + this.converter = converter; + this.operations = operations; + this.accessStrategy = mappingAccessStrategy == null ? this : mappingAccessStrategy; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) + */ + @Override + public Object insert(T instance, Class domainType, Map additionalParameters) { + return insert(instance, domainType, Identifier.from(additionalParameters)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) + */ + @Override + public Object insert(T instance, Class domainType, Identifier identifier) { + + KeyHolder holder = new GeneratedKeyHolder(); + RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); + + MapSqlParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", + PersistentProperty::isIdProperty); + + identifier.forEach((name, value, type) -> addConvertedPropertyValue(parameterSource, name, value, type)); + + Object idValue = getIdValueOrNull(instance, persistentEntity); + if (idValue != null) { + + RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); + addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + } + + operations.update( // + sql(domainType).getInsert(new HashSet<>(Arrays.asList(parameterSource.getParameterNames()))), // + parameterSource, // + holder // + ); + + return getIdFromHolder(holder, persistentEntity); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#update(java.lang.Object, java.lang.Class) + */ + @Override + public boolean update(S instance, Class domainType) { + + RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); + + return operations.update(sql(domainType).getUpdate(), + getParameterSource(instance, persistentEntity, "", Predicates.includeAll())) != 0; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, java.lang.Class) + */ + @Override + public void delete(Object id, Class domainType) { + + String deleteByIdSql = sql(domainType).getDeleteById(); + MapSqlParameterSource parameter = createIdParameterSource(id, domainType); + + operations.update(deleteByIdSql, parameter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PropertyPath) + */ + @Override + public void delete(Object rootId, PersistentPropertyPath propertyPath) { + + RelationalPersistentEntity rootEntity = context + .getRequiredPersistentEntity(propertyPath.getBaseProperty().getOwner().getType()); + + RelationalPersistentProperty referencingProperty = propertyPath.getLeafProperty(); + Assert.notNull(referencingProperty, "No property found matching the PropertyPath " + propertyPath); + + String format = sql(rootEntity.getType()).createDeleteByPath(propertyPath); + + HashMap parameters = new HashMap<>(); + parameters.put("rootId", rootId); + operations.update(format, parameters); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteAll(java.lang.Class) + */ + @Override + public void deleteAll(Class domainType) { + operations.getJdbcOperations().update(sql(domainType).createDeleteAllSql(null)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteAll(org.springframework.data.mapping.PropertyPath) + */ + @Override + public void deleteAll(PersistentPropertyPath propertyPath) { + operations.getJdbcOperations() + .update(sql(propertyPath.getBaseProperty().getOwner().getType()).createDeleteAllSql(propertyPath)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class) + */ + @Override + public long count(Class domainType) { + + Long result = operations.getJdbcOperations().queryForObject(sql(domainType).getCount(), Long.class); + + Assert.notNull(result, "The result of a count query must not be null."); + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#findById(java.lang.Object, java.lang.Class) + */ + @SuppressWarnings("unchecked") + @Override + public T findById(Object id, Class domainType) { + + String findOneSql = sql(domainType).getFindOne(); + MapSqlParameterSource parameter = createIdParameterSource(id, domainType); + + try { + return operations.queryForObject(findOneSql, parameter, (RowMapper) getEntityRowMapper(domainType)); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAll(java.lang.Class) + */ + @SuppressWarnings("unchecked") + @Override + public Iterable findAll(Class domainType) { + return operations.query(sql(domainType).getFindAll(), (RowMapper) getEntityRowMapper(domainType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllById(java.lang.Iterable, java.lang.Class) + */ + @SuppressWarnings("unchecked") + @Override + public Iterable findAllById(Iterable ids, Class domainType) { + + RelationalPersistentProperty idProperty = getRequiredPersistentEntity(domainType).getRequiredIdProperty(); + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + + addConvertedPropertyValuesAsList(parameterSource, idProperty, ids, "ids"); + + String findAllInListSql = sql(domainType).getFindAllInList(); + + return operations.query(findAllInListSql, parameterSource, (RowMapper) getEntityRowMapper(domainType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllByProperty(java.lang.Object, org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty) + */ + @Override + @SuppressWarnings("unchecked") + public Iterable findAllByProperty(Object rootId, RelationalPersistentProperty property) { + + 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))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#existsById(java.lang.Object, java.lang.Class) + */ + @Override + public boolean existsById(Object id, Class domainType) { + + String existsSql = sql(domainType).getExists(); + MapSqlParameterSource parameter = createIdParameterSource(id, domainType); + + Boolean result = operations.queryForObject(existsSql, parameter, Boolean.class); + + Assert.notNull(result, "The result of an exists query must not be null"); + + return result; + } + + private MapSqlParameterSource getParameterSource(S instance, RelationalPersistentEntity persistentEntity, + String prefix, Predicate skipProperty) { + + MapSqlParameterSource parameters = new MapSqlParameterSource(); + + PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(instance); + + persistentEntity.doWithProperties((PropertyHandler) property -> { + + if (skipProperty.test(property)) { + return; + } + if (property.isEntity() && !property.isEmbedded()) { + return; + } + + if (property.isEmbedded()) { + + Object value = propertyAccessor.getProperty(property); + RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getType()); + MapSqlParameterSource additionalParameters = getParameterSource((T) value, + (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty); + parameters.addValues(additionalParameters.getValues()); + } else { + + Object value = propertyAccessor.getProperty(property); + String paramName = prefix + property.getColumnName(); + + addConvertedPropertyValue(parameters, property, value, paramName); + } + }); + + return parameters; + } + + @SuppressWarnings("unchecked") + @Nullable + private ID getIdValueOrNull(S instance, RelationalPersistentEntity persistentEntity) { + + ID idValue = (ID) persistentEntity.getIdentifierAccessor(instance).getIdentifier(); + + return isIdPropertyNullOrScalarZero(idValue, persistentEntity) ? null : idValue; + } + + private static boolean isIdPropertyNullOrScalarZero(@Nullable ID idValue, + RelationalPersistentEntity persistentEntity) { + + RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); + return idValue == null // + || idProperty == null // + || (idProperty.getType() == int.class && idValue.equals(0)) // + || (idProperty.getType() == long.class && idValue.equals(0L)); + } + + @Nullable + private Object getIdFromHolder(KeyHolder holder, RelationalPersistentEntity persistentEntity) { + + try { + // MySQL just returns one value with a special name + return holder.getKey(); + } catch (DataRetrievalFailureException | InvalidDataAccessApiUsageException e) { + // Postgres returns a value for each column + // MS SQL Server returns a value that might be null. + + Map keys = holder.getKeys(); + + if (keys == null || persistentEntity.getIdProperty() == null) { + return null; + } + + return keys.get(persistentEntity.getIdColumn()); + } + } + + private EntityRowMapper getEntityRowMapper(Class domainType) { + return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), converter, accessStrategy); + } + + private RowMapper getMapEntityRowMapper(RelationalPersistentProperty property) { + + String keyColumn = property.getKeyColumn(); + Assert.notNull(keyColumn, () -> "KeyColumn must not be null for " + property); + + return new MapEntityRowMapper<>(getEntityRowMapper(property.getActualType()), keyColumn); + } + + private MapSqlParameterSource createIdParameterSource(Object id, Class domainType) { + + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + + addConvertedPropertyValue( // + parameterSource, // + getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // + id, // + "id" // + ); + return parameterSource; + } + + private void addConvertedPropertyValue(MapSqlParameterSource parameterSource, RelationalPersistentProperty property, + Object value, String paramName) { + + JdbcValue jdbcValue = converter.writeJdbcValue( // + value, // + property.getColumnType(), // + property.getSqlType() // + ); + + parameterSource.addValue(paramName, jdbcValue.getValue(), JdbcUtil.sqlTypeFor(jdbcValue.getJdbcType())); + } + + private void addConvertedPropertyValue(MapSqlParameterSource parameterSource, String name, Object value, + Class type) { + + JdbcValue jdbcValue = converter.writeJdbcValue( // + value, // + type, // + JdbcUtil.sqlTypeFor(type) // + ); + + parameterSource.addValue( // + name, // + jdbcValue.getValue(), // + JdbcUtil.sqlTypeFor(jdbcValue.getJdbcType()) // + ); + } + + private void addConvertedPropertyValuesAsList(MapSqlParameterSource parameterSource, + RelationalPersistentProperty property, Iterable values, String paramName) { + + List convertedIds = new ArrayList<>(); + JdbcValue jdbcValue = null; + for (Object id : values) { + + Class columnType = property.getColumnType(); + int sqlType = property.getSqlType(); + + jdbcValue = converter.writeJdbcValue(id, columnType, sqlType); + convertedIds.add(jdbcValue.getValue()); + } + + Assert.notNull(jdbcValue, "JdbcValue must be not null at this point. Please report this as a bug."); + + JDBCType jdbcType = jdbcValue.getJdbcType(); + int typeNumber = jdbcType == null ? JdbcUtils.TYPE_UNKNOWN : jdbcType.getVendorTypeNumber(); + + parameterSource.addValue(paramName, convertedIds, typeNumber); + } + + @SuppressWarnings("unchecked") + private RelationalPersistentEntity getRequiredPersistentEntity(Class domainType) { + return (RelationalPersistentEntity) context.getRequiredPersistentEntity(domainType); + } + + private SqlGenerator sql(Class domainType) { + return sqlGeneratorSource.getSqlGenerator(domainType); + } + + /** + * Utility to create {@link Predicate}s. + */ + static class Predicates { + + /** + * Include all {@link Predicate} returning {@literal false} to never skip a property. + * + * @return the include all {@link Predicate}. + */ + static Predicate includeAll() { + return it -> false; + } + } +} 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 new file mode 100644 index 00000000..03e9fd4d --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java @@ -0,0 +1,59 @@ +/* + * 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.jdbc.core.convert; + +import java.sql.ResultSet; + +import org.springframework.data.jdbc.core.DataAccessStrategy; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.jdbc.core.RowMapper; + +/** + * Maps a {@link ResultSet} to an entity of type {@code T}, including entities referenced. This {@link RowMapper} might + * trigger additional SQL statements in order to load other members of the same aggregate. + * + * @author Jens Schauder + * @author Oliver Gierke + * @author Mark Paluch + * @author Maciej Walkowiak + * @author Bastian Wilhelm + */ +public class EntityRowMapper implements RowMapper { + + private final RelationalPersistentEntity entity; + + private final JdbcConverter converter; + private final DataAccessStrategy accessStrategy; + + public EntityRowMapper(RelationalPersistentEntity entity, JdbcConverter converter, + DataAccessStrategy accessStrategy) { + + this.entity = entity; + this.converter = converter; + this.accessStrategy = accessStrategy; + } + + /* + * (non-Javadoc) + * @see org.springframework.jdbc.core.RowMapper#mapRow(java.sql.ResultSet, int) + */ + @Override + public T mapRow(ResultSet resultSet, int rowNumber) { + return converter.mapRow(entity, accessStrategy, resultSet); + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/IterableOfEntryToMapConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IterableOfEntryToMapConverter.java similarity index 97% rename from spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/IterableOfEntryToMapConverter.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IterableOfEntryToMapConverter.java index 4f14cf48..742bccaa 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/IterableOfEntryToMapConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IterableOfEntryToMapConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import java.util.HashMap; import java.util.Map; 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 30cff87b..10a56a99 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 @@ -15,10 +15,14 @@ */ package org.springframework.data.jdbc.core.convert; +import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.relational.core.conversion.RelationalConverter; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; +import java.sql.ResultSet; + /** * A {@link JdbcConverter} is responsible for converting for values to the native relational representation and vice * versa. @@ -38,4 +42,10 @@ public interface JdbcConverter extends RelationalConverter { * @return The converted value wrapped in a {@link JdbcValue}. Guaranteed to be not {@literal null}. */ 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); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/MapEntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java similarity index 97% rename from spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/MapEntityRowMapper.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java index dac66569..f4b0b319 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/MapEntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import java.sql.ResultSet; import java.sql.SQLException; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java similarity index 92% rename from spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlContext.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java index aabb3707..70573469 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; +import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.sql.Column; import org.springframework.data.relational.core.sql.SQL; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java similarity index 92% rename from spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index 10df155c..b0618fdf 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import lombok.Value; @@ -34,27 +34,11 @@ import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.MappingContext; +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; -import org.springframework.data.relational.core.sql.AssignValue; -import org.springframework.data.relational.core.sql.Assignments; -import org.springframework.data.relational.core.sql.BindMarker; -import org.springframework.data.relational.core.sql.Column; -import org.springframework.data.relational.core.sql.Condition; -import org.springframework.data.relational.core.sql.Delete; -import org.springframework.data.relational.core.sql.DeleteBuilder; -import org.springframework.data.relational.core.sql.Expression; -import org.springframework.data.relational.core.sql.Expressions; -import org.springframework.data.relational.core.sql.Functions; -import org.springframework.data.relational.core.sql.Insert; -import org.springframework.data.relational.core.sql.InsertBuilder; -import org.springframework.data.relational.core.sql.SQL; -import org.springframework.data.relational.core.sql.Select; -import org.springframework.data.relational.core.sql.SelectBuilder; -import org.springframework.data.relational.core.sql.StatementBuilder; -import org.springframework.data.relational.core.sql.Table; -import org.springframework.data.relational.core.sql.Update; +import org.springframework.data.relational.core.sql.*; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; @@ -105,6 +89,48 @@ class SqlGenerator { this.columns = new Columns(entity, mappingContext); } + /** + * Construct a IN-condition based on a {@link Select Sub-Select} which selects the ids (or stand ins for ids) of the + * given {@literal path} to those that reference the root entities specified by the {@literal rootCondition}. + * + * @param path specifies the table and id to select + * @param rootCondition the condition on the root of the path determining what to select + * @param filterColumn the column to apply the IN-condition to. + * @return the IN condition + */ + private static Condition getSubselectCondition(PersistentPropertyPathExtension path, + Function rootCondition, Column filterColumn) { + + PersistentPropertyPathExtension parentPath = path.getParentPath(); + + if (!parentPath.hasIdProperty()) { + if (parentPath.getLength() > 1) { + return getSubselectCondition(parentPath, rootCondition, filterColumn); + } + return rootCondition.apply(filterColumn); + } + + Table subSelectTable = SQL.table(parentPath.getTableName()); + Column idColumn = subSelectTable.column(parentPath.getIdColumnName()); + Column selectFilterColumn = subSelectTable.column(parentPath.getEffectiveIdColumnName()); + + Condition innerCondition = parentPath.getLength() == 1 // if the parent is the root of the path + ? rootCondition.apply(selectFilterColumn) // apply the rootCondition + : getSubselectCondition(parentPath, rootCondition, selectFilterColumn); // otherwise we need another layer of + // subselect + + Select select = Select.builder() // + .select(idColumn) // + .from(subSelectTable) // + .where(innerCondition).build(); + + return filterColumn.in(select); + } + + private static BindMarker getBindMarker(String columnName) { + return SQL.bindMarker(":" + parameterPattern.matcher(columnName).replaceAll("")); + } + /** * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. * Results are filtered using an {@code IN}-clause on the id column. @@ -467,34 +493,6 @@ class SqlGenerator { return render(delete); } - /** - * Construct a {@link Select Sub-Select}. - * - * @param path - * @param rootCondition - * @param filterColumn - * @return - */ - private static Condition getSubselectCondition(PersistentPropertyPathExtension path, - Function rootCondition, Column filterColumn) { - - PersistentPropertyPathExtension parentPath = path.getParentPath(); - - Table subSelectTable = SQL.table(parentPath.getTableName()); - Column idColumn = subSelectTable.column(parentPath.getIdColumnName()); - Column selectFilterColumn = subSelectTable.column(parentPath.getEffectiveIdColumnName()); - - Condition innerCondition = parentPath.getLength() == 1 ? rootCondition.apply(selectFilterColumn) - : getSubselectCondition(parentPath, rootCondition, selectFilterColumn); - - Select select = Select.builder() // - .select(idColumn) // - .from(subSelectTable) // - .where(innerCondition).build(); - - return filterColumn.in(select); - } - private String createDeleteByListSql() { Table table = getTable(); @@ -531,10 +529,6 @@ class SqlGenerator { return sqlContext.getIdColumn(); } - private static BindMarker getBindMarker(String columnName) { - return SQL.bindMarker(":" + parameterPattern.matcher(columnName).replaceAll("")); - } - /** * Value object representing a {@code JOIN} association. */ diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java similarity index 96% rename from spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java index 732d9807..785a2417 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import lombok.RequiredArgsConstructor; 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 f19ec092..1cdba413 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,13 +22,12 @@ import java.util.Map; import org.apache.ibatis.session.SqlSession; import org.mybatis.spring.SqlSessionTemplate; - import org.springframework.data.jdbc.core.CascadingDataAccessStrategy; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy; -import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -102,7 +101,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { *

* Use a {@link SqlSessionTemplate} for {@link SqlSession} or a similar implementation tying the session to the proper * transaction. Note that the resulting {@link DataAccessStrategy} only handles MyBatis. It does not include the - * functionality of the {@link org.springframework.data.jdbc.core.DefaultDataAccessStrategy} which one normally still + * functionality of the {@link DefaultDataAccessStrategy} which one normally still * wants. Use * {@link #createCombinedAccessStrategy(RelationalMappingContext, JdbcConverter, NamedParameterJdbcOperations, SqlSession, NamespaceStrategy)} * to create such a {@link DataAccessStrategy}. 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 cd34a5b4..bdd10ee5 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 @@ -22,13 +22,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; -import org.springframework.data.jdbc.core.SqlGeneratorSource; 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.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.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.NamingStrategy; @@ -79,8 +79,9 @@ public abstract class AbstractJdbcConfiguration { /** * Register custom {@link Converter}s in a {@link JdbcCustomConversions} object if required. These - * {@link JdbcCustomConversions} will be registered with the {@link #jdbcConverter(RelationalMappingContext, JdbcOperations)}. - * Returns an empty {@link JdbcCustomConversions} instance by default. + * {@link JdbcCustomConversions} will be registered with the + * {@link #jdbcConverter(RelationalMappingContext, JdbcOperations)}. Returns an empty {@link JdbcCustomConversions} + * instance by default. * * @return must not be {@literal null}. */ 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 c9457830..6835aee5 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 @@ -22,14 +22,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; -import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +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.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.NamingStrategy; 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 e6dcd2dc..d604c03f 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 @@ -19,10 +19,10 @@ import java.lang.reflect.Method; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.EntityRowMapper; +import org.springframework.data.jdbc.core.convert.EntityRowMapper; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.repository.core.NamedQueries; @@ -47,7 +47,7 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { private final ApplicationEventPublisher publisher; private final RelationalMappingContext context; - private final RelationalConverter converter; + private final JdbcConverter converter; private final DataAccessStrategy accessStrategy; private final QueryMappingConfiguration queryMappingConfiguration; private final NamedParameterJdbcOperations operations; @@ -63,8 +63,8 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { * @param queryMappingConfiguration must not be {@literal null}. */ JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, RelationalMappingContext context, - RelationalConverter converter, DataAccessStrategy accessStrategy, - QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations) { + JdbcConverter converter, DataAccessStrategy accessStrategy, QueryMappingConfiguration queryMappingConfiguration, + NamedParameterJdbcOperations operations) { Assert.notNull(publisher, "Publisher must not be null!"); Assert.notNull(context, "RelationalMappingContext must not be null!"); @@ -118,7 +118,7 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { EntityRowMapper defaultEntityRowMapper = new EntityRowMapper<>( // context.getRequiredPersistentEntity(domainType), // - context, // + // converter, // accessStrategy); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index e15a12a1..65601b0b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -20,9 +20,9 @@ import java.util.Optional; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.jdbc.repository.RowMapperMap; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.repository.core.EntityInformation; @@ -47,7 +47,7 @@ import org.springframework.util.Assert; public class JdbcRepositoryFactory extends RepositoryFactorySupport { private final RelationalMappingContext context; - private final RelationalConverter converter; + private final JdbcConverter converter; private final ApplicationEventPublisher publisher; private final DataAccessStrategy accessStrategy; private final NamedParameterJdbcOperations operations; @@ -57,15 +57,14 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { /** * Creates a new {@link JdbcRepositoryFactory} for the given {@link DataAccessStrategy}, * {@link RelationalMappingContext} and {@link ApplicationEventPublisher}. - * - * @param dataAccessStrategy must not be {@literal null}. + * @param dataAccessStrategy must not be {@literal null}. * @param context must not be {@literal null}. * @param converter must not be {@literal null}. * @param publisher must not be {@literal null}. * @param operations must not be {@literal null}. */ public JdbcRepositoryFactory(DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, - RelationalConverter converter, ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations) { + JdbcConverter converter, ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations) { Assert.notNull(dataAccessStrategy, "DataAccessStrategy must not be null!"); Assert.notNull(context, "RelationalMappingContext must not be null!"); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index 23121295..0860fbe7 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -22,9 +22,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.relational.core.mapping.RelationalMappingContext; 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 11efc3ff..20748a8f 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 @@ -25,9 +25,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.DbAction.Insert; import org.springframework.data.relational.core.conversion.DbAction.InsertRoot; -import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.Identifier; /** @@ -38,14 +36,9 @@ import org.springframework.data.relational.domain.Identifier; public class DefaultJdbcInterpreterUnitTests { static final long CONTAINER_ID = 23L; - static final String BACK_REFERENCE = "back-reference"; + static final String BACK_REFERENCE = "container"; - RelationalMappingContext context = new JdbcMappingContext(new NamingStrategy() { - @Override - public String getReverseColumnName(RelationalPersistentProperty property) { - return BACK_REFERENCE; - } - }); + RelationalMappingContext context = new JdbcMappingContext(); DataAccessStrategy dataAccessStrategy = mock(DataAccessStrategy.class); DefaultJdbcInterpreter interpreter = new DefaultJdbcInterpreter(context, dataAccessStrategy); @@ -54,8 +47,10 @@ public class DefaultJdbcInterpreterUnitTests { Element element = new Element(); InsertRoot containerInsert = new InsertRoot<>(container); - Insert insert = new Insert<>(element, PropertyPathTestingUtils.toPath("element", Container.class, context), + Insert elementInsert = new Insert<>(element, PropertyPathTestingUtils.toPath("element", Container.class, context), containerInsert); + Insert element1Insert = new Insert<>(element, PropertyPathTestingUtils.toPath("element.element1", Container.class, context), + elementInsert); @Test // DATAJDBC-145 public void insertDoesHonourNamingStrategyForBackReference() { @@ -63,7 +58,7 @@ public class DefaultJdbcInterpreterUnitTests { container.id = CONTAINER_ID; containerInsert.setGeneratedId(CONTAINER_ID); - interpreter.interpret(insert); + interpreter.interpret(elementInsert); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); @@ -78,7 +73,7 @@ public class DefaultJdbcInterpreterUnitTests { container.id = CONTAINER_ID; - interpreter.interpret(insert); + interpreter.interpret(elementInsert); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); @@ -93,7 +88,22 @@ public class DefaultJdbcInterpreterUnitTests { containerInsert.setGeneratedId(CONTAINER_ID); - interpreter.interpret(insert); + interpreter.interpret(elementInsert); + + 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(BACK_REFERENCE, CONTAINER_ID, Long.class)); + } + + @Test // DATAJDBC-359 + public void generatedIdOfParentsParentGetsPassedOnAsAdditionalParameter() { + + containerInsert.setGeneratedId(CONTAINER_ID); + + interpreter.interpret(element1Insert); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); @@ -111,5 +121,11 @@ public class DefaultJdbcInterpreterUnitTests { Element element; } - static class Element {} + @SuppressWarnings("unused") + static class Element { + Element1 element1; + } + + static class Element1 { + } } 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 dcbb0855..7747c2fa 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 @@ -20,8 +20,6 @@ import static org.assertj.core.api.Assertions.*; import lombok.Data; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -45,8 +43,6 @@ import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.annotation.ProfileValueSourceConfiguration; @@ -71,8 +67,7 @@ public class JdbcAggregateTemplateIntegrationTests { @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); @Rule public SpringMethodRule methodRule = new SpringMethodRule(); @Autowired JdbcAggregateOperations template; - @Autowired - NamedParameterJdbcOperations jdbcTemplate; + @Autowired NamedParameterJdbcOperations jdbcTemplate; LegoSet legoSet = createLegoSet(); @Test // DATAJDBC-112 @@ -465,8 +460,39 @@ public class JdbcAggregateTemplateIntegrationTests { template.delete(chain4, Chain4.class); - String countSelect = "SELECT COUNT(*) FROM %s"; - jdbcTemplate.queryForObject(String.format(countSelect, "CHAIN0"),emptyMap(), Long.class); + assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM CHAIN0", emptyMap(), Long.class)) // + .isEqualTo(0); + } + + @Test // DATAJDBC-359 + public void saveAndLoadLongChainWithoutIds() { + + NoIdChain4 chain4 = new NoIdChain4(); + chain4.fourValue = "omega"; + chain4.chain3 = new NoIdChain3(); + chain4.chain3.threeValue = "delta"; + chain4.chain3.chain2 = new NoIdChain2(); + chain4.chain3.chain2.twoValue = "gamma"; + chain4.chain3.chain2.chain1 = new NoIdChain1(); + chain4.chain3.chain2.chain1.oneValue = "beta"; + chain4.chain3.chain2.chain1.chain0 = new NoIdChain0(); + chain4.chain3.chain2.chain1.chain0.zeroValue = "alpha"; + + template.save(chain4); + + assertThat(chain4.four).isNotNull(); + + NoIdChain4 reloaded = template.findById(chain4.four, NoIdChain4.class); + + assertThat(reloaded).isNotNull(); + + assertThat(reloaded.four).isEqualTo(chain4.four); + assertThat(reloaded.chain3.chain2.chain1.chain0.zeroValue).isEqualTo(chain4.chain3.chain2.chain1.chain0.zeroValue); + + template.delete(chain4, NoIdChain4.class); + + assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM CHAIN0", emptyMap(), Long.class)) // + .isEqualTo(0); } private static void assumeNot(String dbProfileName) { @@ -573,6 +599,9 @@ public class JdbcAggregateTemplateIntegrationTests { } } + /** + * One may think of ChainN as a chain with N further elements + */ static class Chain0 { @Id Long zero; String zeroValue; @@ -601,4 +630,32 @@ public class JdbcAggregateTemplateIntegrationTests { String fourValue; Chain3 chain3; } + + /** + * One may think of ChainN as a chain with N further elements + */ + static class NoIdChain0 { + String zeroValue; + } + + static class NoIdChain1 { + String oneValue; + NoIdChain0 chain0; + } + + static class NoIdChain2 { + String twoValue; + NoIdChain1 chain1; + } + + static class NoIdChain3 { + String threeValue; + NoIdChain2 chain2; + } + + static class NoIdChain4 { + @Id Long four; + String fourValue; + NoIdChain3 chain3; + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java index 27350c3e..24170e35 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java @@ -17,15 +17,18 @@ package org.springframework.data.jdbc.core; import java.util.List; -import org.assertj.core.api.SoftAssertions; import org.jetbrains.annotations.NotNull; import org.junit.Test; +import org.springframework.data.annotation.Id; 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.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import static org.assertj.core.api.SoftAssertions.*; + /** * @author Jens Schauder */ @@ -37,23 +40,23 @@ public class PersistentPropertyPathExtensionUnitTests { @Test public void isEmbedded() { - SoftAssertions.assertSoftly(softly -> { + assertSoftly(softly -> { softly.assertThat(extPath(entity).isEmbedded()).isFalse(); softly.assertThat(extPath("second").isEmbedded()).isFalse(); - softly.assertThat(extPath("second.third").isEmbedded()).isTrue(); + softly.assertThat(extPath("second.third2").isEmbedded()).isTrue(); }); } @Test public void isMultiValued() { - SoftAssertions.assertSoftly(softly -> { + assertSoftly(softly -> { softly.assertThat(extPath(entity).isMultiValued()).isFalse(); softly.assertThat(extPath("second").isMultiValued()).isFalse(); - softly.assertThat(extPath("second.third").isMultiValued()).isFalse(); - softly.assertThat(extPath("secondList.third").isMultiValued()).isTrue(); + softly.assertThat(extPath("second.third2").isMultiValued()).isFalse(); + softly.assertThat(extPath("secondList.third2").isMultiValued()).isTrue(); softly.assertThat(extPath("secondList").isMultiValued()).isTrue(); }); } @@ -64,12 +67,12 @@ public class PersistentPropertyPathExtensionUnitTests { RelationalPersistentEntity second = context.getRequiredPersistentEntity(Second.class); RelationalPersistentEntity third = context.getRequiredPersistentEntity(Third.class); - SoftAssertions.assertSoftly(softly -> { + assertSoftly(softly -> { softly.assertThat(extPath(entity).getLeafEntity()).isEqualTo(entity); softly.assertThat(extPath("second").getLeafEntity()).isEqualTo(second); - softly.assertThat(extPath("second.third").getLeafEntity()).isEqualTo(third); - softly.assertThat(extPath("secondList.third").getLeafEntity()).isEqualTo(third); + softly.assertThat(extPath("second.third2").getLeafEntity()).isEqualTo(third); + softly.assertThat(extPath("secondList.third2").getLeafEntity()).isEqualTo(third); softly.assertThat(extPath("secondList").getLeafEntity()).isEqualTo(second); }); } @@ -77,15 +80,15 @@ public class PersistentPropertyPathExtensionUnitTests { @Test public void isEntity() { - SoftAssertions.assertSoftly(softly -> { + assertSoftly(softly -> { softly.assertThat(extPath(entity).isEntity()).isTrue(); String path = "second"; softly.assertThat(extPath(path).isEntity()).isTrue(); - softly.assertThat(extPath("second.third").isEntity()).isTrue(); - softly.assertThat(extPath("second.third.value").isEntity()).isFalse(); - softly.assertThat(extPath("secondList.third").isEntity()).isTrue(); - softly.assertThat(extPath("secondList.third.value").isEntity()).isFalse(); + softly.assertThat(extPath("second.third2").isEntity()).isTrue(); + softly.assertThat(extPath("second.third2.value").isEntity()).isFalse(); + softly.assertThat(extPath("secondList.third2").isEntity()).isTrue(); + softly.assertThat(extPath("secondList.third2.value").isEntity()).isFalse(); softly.assertThat(extPath("secondList").isEntity()).isTrue(); }); } @@ -93,14 +96,14 @@ public class PersistentPropertyPathExtensionUnitTests { @Test public void getTableName() { - SoftAssertions.assertSoftly(softly -> { + assertSoftly(softly -> { softly.assertThat(extPath(entity).getTableName()).isEqualTo("dummy_entity"); softly.assertThat(extPath("second").getTableName()).isEqualTo("second"); - softly.assertThat(extPath("second.third").getTableName()).isEqualTo("second"); - softly.assertThat(extPath("second.third.value").getTableName()).isEqualTo("second"); - softly.assertThat(extPath("secondList.third").getTableName()).isEqualTo("second"); - softly.assertThat(extPath("secondList.third.value").getTableName()).isEqualTo("second"); + softly.assertThat(extPath("second.third2").getTableName()).isEqualTo("second"); + softly.assertThat(extPath("second.third2.value").getTableName()).isEqualTo("second"); + softly.assertThat(extPath("secondList.third2").getTableName()).isEqualTo("second"); + softly.assertThat(extPath("secondList.third2.value").getTableName()).isEqualTo("second"); softly.assertThat(extPath("secondList").getTableName()).isEqualTo("second"); }); } @@ -108,34 +111,91 @@ public class PersistentPropertyPathExtensionUnitTests { @Test public void getTableAlias() { - SoftAssertions.assertSoftly(softly -> { + assertSoftly(softly -> { softly.assertThat(extPath(entity).getTableAlias()).isEqualTo(null); softly.assertThat(extPath("second").getTableAlias()).isEqualTo("second"); - softly.assertThat(extPath("second.third").getTableAlias()).isEqualTo("second"); - softly.assertThat(extPath("second.third.value").getTableAlias()).isEqualTo("second"); - softly.assertThat(extPath("second.third2").getTableAlias()).isEqualTo("second_third2"); - softly.assertThat(extPath("second.third2.value").getTableAlias()).isEqualTo("second_third2"); - softly.assertThat(extPath("secondList.third").getTableAlias()).isEqualTo("secondList"); - softly.assertThat(extPath("secondList.third.value").getTableAlias()).isEqualTo("secondList"); - softly.assertThat(extPath("secondList.third2").getTableAlias()).isEqualTo("secondList_third2"); - softly.assertThat(extPath("secondList.third2.value").getTableAlias()).isEqualTo("secondList_third2"); + softly.assertThat(extPath("second.third2").getTableAlias()).isEqualTo("second"); + softly.assertThat(extPath("second.third2.value").getTableAlias()).isEqualTo("second"); + softly.assertThat(extPath("second.third").getTableAlias()).isEqualTo("second_third"); + softly.assertThat(extPath("second.third.value").getTableAlias()).isEqualTo("second_third"); + softly.assertThat(extPath("secondList.third2").getTableAlias()).isEqualTo("secondList"); + softly.assertThat(extPath("secondList.third2.value").getTableAlias()).isEqualTo("secondList"); + softly.assertThat(extPath("secondList.third").getTableAlias()).isEqualTo("secondList_third"); + softly.assertThat(extPath("secondList.third.value").getTableAlias()).isEqualTo("secondList_third"); softly.assertThat(extPath("secondList").getTableAlias()).isEqualTo("secondList"); - softly.assertThat(extPath("second2.third2").getTableAlias()).isEqualTo("secthird2"); + softly.assertThat(extPath("second2.third").getTableAlias()).isEqualTo("secthird"); }); } @Test public void getColumnName() { - SoftAssertions.assertSoftly(softly -> { + assertSoftly(softly -> { - softly.assertThat(extPath("second.third.value").getColumnName()).isEqualTo("thrdvalue"); - softly.assertThat(extPath("second.third2.value").getColumnName()).isEqualTo("value"); - softly.assertThat(extPath("secondList.third.value").getColumnName()).isEqualTo("thrdvalue"); - softly.assertThat(extPath("secondList.third2.value").getColumnName()).isEqualTo("value"); - softly.assertThat(extPath("second2.third.value").getColumnName()).isEqualTo("secthrdvalue"); - softly.assertThat(extPath("second2.third2.value").getColumnName()).isEqualTo("value"); + softly.assertThat(extPath("second.third2.value").getColumnName()).isEqualTo("thrdvalue"); + softly.assertThat(extPath("second.third.value").getColumnName()).isEqualTo("value"); + softly.assertThat(extPath("secondList.third2.value").getColumnName()).isEqualTo("thrdvalue"); + softly.assertThat(extPath("secondList.third.value").getColumnName()).isEqualTo("value"); + softly.assertThat(extPath("second2.third2.value").getColumnName()).isEqualTo("secthrdvalue"); + softly.assertThat(extPath("second2.third.value").getColumnName()).isEqualTo("value"); + }); + } + + @Test // DATAJDBC-359 + public void idDefiningPath() { + + assertSoftly(softly -> { + + softly.assertThat(extPath("second.third2.value").getIdDefiningParentPath().getLength()).isEqualTo(0); + softly.assertThat(extPath("second.third.value").getIdDefiningParentPath().getLength()).isEqualTo(0); + softly.assertThat(extPath("secondList.third2.value").getIdDefiningParentPath().getLength()).isEqualTo(0); + softly.assertThat(extPath("secondList.third.value").getIdDefiningParentPath().getLength()).isEqualTo(0); + softly.assertThat(extPath("second2.third2.value").getIdDefiningParentPath().getLength()).isEqualTo(0); + softly.assertThat(extPath("second2.third.value").getIdDefiningParentPath().getLength()).isEqualTo(0); + softly.assertThat(extPath("withId.second.third2.value").getIdDefiningParentPath().getLength()).isEqualTo(1); + softly.assertThat(extPath("withId.second.third.value").getIdDefiningParentPath().getLength()).isEqualTo(1); + }); + } + + @Test // DATAJDBC-359 + public void reverseColumnName() { + + assertSoftly(softly -> { + + softly.assertThat(extPath("second.third2").getReverseColumnName()).isEqualTo("dummy_entity"); + softly.assertThat(extPath("second.third").getReverseColumnName()).isEqualTo("dummy_entity"); + softly.assertThat(extPath("secondList.third2").getReverseColumnName()).isEqualTo("dummy_entity"); + softly.assertThat(extPath("secondList.third").getReverseColumnName()).isEqualTo("dummy_entity"); + softly.assertThat(extPath("second2.third2").getReverseColumnName()).isEqualTo("dummy_entity"); + softly.assertThat(extPath("second2.third").getReverseColumnName()).isEqualTo("dummy_entity"); + softly.assertThat(extPath("withId.second.third2.value").getReverseColumnName()).isEqualTo("with_id"); + softly.assertThat(extPath("withId.second.third").getReverseColumnName()).isEqualTo("with_id"); + softly.assertThat(extPath("withId.second2.third").getReverseColumnName()).isEqualTo("with_id"); + }); + } + + @Test // DATAJDBC-359 + public void getRequiredIdProperty() { + + assertSoftly(softly -> { + + softly.assertThat(extPath(entity).getRequiredIdProperty().getName()).isEqualTo("entityId"); + softly.assertThat(extPath("withId").getRequiredIdProperty().getName()).isEqualTo("withIdId"); + softly.assertThatThrownBy(() -> extPath("second").getRequiredIdProperty()) + .isInstanceOf(IllegalStateException.class); + }); + } + + @Test // DATAJDBC-359 + public void extendBy() { + + assertSoftly(softly -> { + + softly.assertThat(extPath(entity).extendBy(entity.getRequiredPersistentProperty("withId"))) + .isEqualTo(extPath("withId")); + softly.assertThat(extPath("withId").extendBy(extPath("withId").getRequiredIdProperty())) + .isEqualTo(extPath("withId.withIdId")); }); } @@ -155,15 +215,17 @@ public class PersistentPropertyPathExtensionUnitTests { @SuppressWarnings("unused") static class DummyEntity { + @Id Long entityId; Second second; - List secondList; @Embedded("sec") Second second2; + List secondList; + WithId withId; } @SuppressWarnings("unused") static class Second { - @Embedded("thrd") Third third; - Third third2; + Third third; + @Embedded("thrd") Third third2; } @SuppressWarnings("unused") @@ -171,4 +233,11 @@ public class PersistentPropertyPathExtensionUnitTests { String value; } + @SuppressWarnings("unused") + static class WithId { + @Id Long withIdId; + Second second; + @Embedded("sec") Second second2; + } + } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathTestingUtils.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathTestingUtils.java index 0dcf7f42..81d4a486 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathTestingUtils.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathTestingUtils.java @@ -29,10 +29,10 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProp * @author Jens Schauder */ @UtilityClass -class PropertyPathTestingUtils { +public class PropertyPathTestingUtils { - static PersistentPropertyPath toPath(String path, Class source, - RelationalMappingContext context) { + public static PersistentPropertyPath toPath(String path, Class source, + RelationalMappingContext context) { PersistentPropertyPaths persistentPropertyPaths = context .findPersistentPropertyPaths(source, p -> true); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java similarity index 97% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java index c52acd3a..0b194329 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; @@ -36,6 +36,7 @@ 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; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java similarity index 73% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java index 8a7bdcf8..d491ac09 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +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; @@ -34,18 +34,18 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import javax.naming.OperationNotSupportedException; +import org.assertj.core.api.SoftAssertions; import org.junit.Test; 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.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; -import org.springframework.data.relational.core.conversion.BasicRelationalConverter; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -213,7 +213,7 @@ public class EntityRowMapperUnitTests { @Test // DATAJDBC-252 public void doesNotTryToSetPropertiesThatAreSetViaConstructor() throws SQLException { - ResultSet rs = mockResultSet(asList("value"), // + ResultSet rs = mockResultSet(singletonList("value"), // "value-from-resultSet"); rs.next(); @@ -240,7 +240,7 @@ public class EntityRowMapperUnitTests { @Test // DATAJDBC-273 public void handlesNonSimplePropertyInConstructor() throws SQLException { - ResultSet rs = mockResultSet(asList("id"), // + ResultSet rs = mockResultSet(singletonList("id"), // ID_FOR_ENTITY_REFERENCING_LIST); rs.next(); @@ -249,6 +249,46 @@ public class EntityRowMapperUnitTests { assertThat(extracted.content).hasSize(2); } + @Test // DATAJDBC-359 + public void chainedEntitiesWithoutId() throws SQLException { + + // @formatter:off + Fixture fixture = this. buildFixture() // + // Id of the aggregate root and backreference to it from + // the various aggregate members. + .value(4L).inColumns("four", // + "chain3_no_id_chain4", // + "chain3_chain2_no_id_chain4", // + "chain3_chain2_chain1_no_id_chain4", // + "chain3_chain2_chain1_chain0_no_id_chain4") // + .endUpIn(e -> e.four) + // values for the different entities + .value("four_value").inColumns("four_value").endUpIn(e -> e.fourValue) // + + .value("three_value").inColumns("chain3_three_value").endUpIn(e -> e.chain3.threeValue) // + + .value("two_value").inColumns("chain3_chain2_two_value").endUpIn(e -> e.chain3.chain2.twoValue) // + + .value("one_value").inColumns("chain3_chain2_chain1_one_value").endUpIn(e -> e.chain3.chain2.chain1.oneValue) // + + .value("zero_value").inColumns("chain3_chain2_chain1_chain0_zero_value") + .endUpIn(e -> e.chain3.chain2.chain1.chain0.zeroValue) // + .build(); + // @formatter:on + + ResultSet rs = fixture.resultSet; + + rs.next(); + + NoIdChain4 extracted = createRowMapper(NoIdChain4.class).mapRow(rs, 1); + + fixture.assertOn(extracted); + } + + private FixtureBuilder buildFixture() { + return new FixtureBuilder<>(); + } + private EntityRowMapper createRowMapper(Class type) { return createRowMapper(type, NamingStrategy.INSTANCE); } @@ -275,11 +315,11 @@ public class EntityRowMapperUnitTests { ))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_LIST), any(RelationalPersistentProperty.class)); - RelationalConverter converter = new BasicRelationalConverter(context, new JdbcCustomConversions()); + JdbcConverter converter = new BasicJdbcConverter(context, new JdbcCustomConversions()); return new EntityRowMapper<>( // (RelationalPersistentEntity) context.getRequiredPersistentEntity(type), // - context, // + // converter, // accessStrategy // ); @@ -335,27 +375,22 @@ public class EntityRowMapperUnitTests { switch (invocation.getMethod().getName()) { case "next": + return next(); + case "getObject": + return getObject(invocation.getArgument(0)); + case "isAfterLast": + return isAfterLast(); + case "isBeforeFirst": + return isBeforeFirst(); + case "getRow": + return isAfterLast() || isBeforeFirst() ? 0 : index + 1; + case "toString": + return this.toString(); + default: + throw new OperationNotSupportedException(invocation.getMethod().getName()); + } - if (invocation.getMethod().getName().equals("next")) - return next(); - - if (invocation.getMethod().getName().equals("getObject")) - return getObject(invocation.getArgument(0)); - - if (invocation.getMethod().getName().equals("isAfterLast")) - return isAfterLast(); - - if (invocation.getMethod().getName().equals("isBeforeFirst")) - return isBeforeFirst(); - - if (invocation.getMethod().getName().equals("getRow")) - return isAfterLast() || isBeforeFirst() ? 0 : index + 1; - - if (invocation.getMethod().getName().equals("toString")) - return this.toString(); - - throw new OperationNotSupportedException(invocation.getMethod().getName()); } private boolean isAfterLast() { @@ -481,4 +516,121 @@ public class EntityRowMapperUnitTests { final List content; } + + static class NoIdChain0 { + String zeroValue; + } + + static class NoIdChain1 { + String oneValue; + NoIdChain0 chain0; + } + + static class NoIdChain2 { + String twoValue; + NoIdChain1 chain1; + } + + static class NoIdChain3 { + String threeValue; + NoIdChain2 chain2; + } + + static class NoIdChain4 { + @Id Long four; + String fourValue; + NoIdChain3 chain3; + } + + private interface SetValue { + SetColumns value(Object value); + + Fixture build(); + } + + private interface SetColumns { + + SetExpectation inColumns(String... columns); + } + + private interface SetExpectation { + SetValue endUpIn(Function extractor); + } + + private static class FixtureBuilder implements SetValue, SetColumns, SetExpectation { + + private List values = new ArrayList<>(); + private List columns = new ArrayList<>(); + private String explainingColumn; + private List> expectations = new ArrayList<>(); + + @Override + public SetColumns value(Object value) { + + values.add(value); + + return this; + } + + @Override + public SetExpectation inColumns(String... columns) { + + boolean isFirst = true; + for (String column : columns) { + + // if more than one column is mentioned, we need to copy the value for all but the first column; + if (!isFirst) { + + values.add(values.get(values.size() - 1)); + } else { + + explainingColumn = column; + isFirst = false; + } + + this.columns.add(column); + } + + return this; + } + + @Override + public Fixture build() { + + return new Fixture<>(mockResultSet(columns, values.toArray()), expectations); + } + + @Override + public SetValue endUpIn(Function extractor) { + + expectations.add(new Expectation(extractor, values.get(values.size() - 1), explainingColumn)); + return this; + } + } + + @AllArgsConstructor + private static class Fixture { + final ResultSet resultSet; + final List> expectations; + + public void assertOn(T result) { + + SoftAssertions.assertSoftly(softly -> { + expectations.forEach(expectation -> { + + softly.assertThat(expectation.extractor.apply(result)).describedAs("From column: " + expectation.sourceColumn) + .isEqualTo(expectation.expectedValue); + }); + + }); + } + } + + @AllArgsConstructor + private static class Expectation { + + final Function extractor; + final Object expectedValue; + final String sourceColumn; + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/IterableOfEntryToMapConverterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IterableOfEntryToMapConverterUnitTests.java similarity index 97% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/IterableOfEntryToMapConverterUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IterableOfEntryToMapConverterUnitTests.java index abafe573..75b5a1d1 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/IterableOfEntryToMapConverterUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IterableOfEntryToMapConverterUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import static java.util.Arrays.*; import static java.util.Collections.*; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java similarity index 75% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java index 90db074d..df2d1b97 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.jdbc.core.PropertyPathTestingUtils.*; @@ -22,13 +22,11 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import org.jetbrains.annotations.NotNull; import org.junit.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; +import org.springframework.data.jdbc.core.JdbcIdentifierBuilder; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; -import org.springframework.data.mapping.PersistentPropertyPath; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.domain.Identifier; /** @@ -55,7 +53,7 @@ public class JdbcIdentifierBuilderUnitTests { @Test // DATAJDBC-326 public void qualifiersForMaps() { - PersistentPropertyPath path = getPath("children"); + PersistentPropertyPathExtension path = getPath("children"); Identifier identifier = JdbcIdentifierBuilder // .forBackReferences(path, "parent-eins") // @@ -73,7 +71,7 @@ public class JdbcIdentifierBuilderUnitTests { @Test // DATAJDBC-326 public void qualifiersForLists() { - PersistentPropertyPath path = getPath("moreChildren"); + PersistentPropertyPathExtension path = getPath("moreChildren"); Identifier identifier = JdbcIdentifierBuilder // .forBackReferences(path, "parent-eins") // @@ -98,13 +96,26 @@ public class JdbcIdentifierBuilderUnitTests { assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // .containsExactly( // - tuple("embeddable", "parent-eins", UUID.class) // + tuple("dummy_entity", "parent-eins", UUID.class) // ); } - @NotNull - private PersistentPropertyPath getPath(String dotPath) { - return toPath(dotPath, DummyEntity.class, context); + @Test // DATAJDBC-326 + public void backreferenceAcrossNoId() { + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(getPath("noId.child"), "parent-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple("dummy_entity", "parent-eins", UUID.class) // + ); + } + + private PersistentPropertyPathExtension getPath(String dotPath) { + return new PersistentPropertyPathExtension(context, toPath(dotPath, DummyEntity.class, context)); } @SuppressWarnings("unused") @@ -120,6 +131,8 @@ public class JdbcIdentifierBuilderUnitTests { List moreChildren; Embeddable embeddable; + + NoId noId; } @SuppressWarnings("unused") @@ -127,5 +140,10 @@ public class JdbcIdentifierBuilderUnitTests { Child child; } + @SuppressWarnings("unused") + static class NoId { + Child child; + } + static class Child {} } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java similarity index 98% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java index 4bf0a597..dbe5cd09 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import static org.assertj.core.api.Assertions.*; @@ -25,6 +25,7 @@ import java.util.function.Consumer; import org.assertj.core.api.SoftAssertions; import org.junit.Test; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.convert.SqlGenerator; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; import org.springframework.data.mapping.PersistentPropertyPath; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorEmbeddedUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java similarity index 97% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorEmbeddedUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java index 10184c76..33e4f379 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorEmbeddedUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; @@ -23,9 +23,11 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.PropertyPathTestingUtils; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; +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.sql.Aliased; @@ -206,7 +208,7 @@ public class SqlGeneratorEmbeddedUnitTests { softly.assertThat(join.getJoinTable().getName()).isEqualTo("other_entity"); softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo("embedded_with_reference"); + softly.assertThat(join.getJoinColumn().getName()).isEqualTo("dummy_entity2"); softly.assertThat(join.getParentId().getName()).isEqualTo("id"); softly.assertThat(join.getParentId().getTable().getName()).isEqualTo("dummy_entity2"); }); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java similarity index 98% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java index 2dd17b91..bc50a366 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import static org.assertj.core.api.Assertions.*; import org.assertj.core.api.SoftAssertions; import org.junit.Test; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.convert.SqlGenerator; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; import org.springframework.data.mapping.PersistentPropertyPath; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java similarity index 92% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index a7e6e064..aa15d93b 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.jdbc.core.convert; import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; @@ -24,15 +24,16 @@ import java.util.Set; import org.assertj.core.api.SoftAssertions; import org.junit.Before; import org.junit.Test; - import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; +import org.springframework.data.jdbc.core.PropertyPathTestingUtils; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.NamingStrategy; +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; @@ -357,6 +358,31 @@ public class SqlGeneratorUnitTests { ")))"); } + @Test // DATAJDBC-359 + public void deletingLongChainNoId() { + + assertThat(createSqlGenerator(NoIdChain4.class) + .createDeleteByPath(getPath("chain3.chain2.chain1.chain0", NoIdChain4.class))) // + .isEqualTo("DELETE FROM no_id_chain0 WHERE no_id_chain0.no_id_chain4 = :rootId"); + } + + @Test // DATAJDBC-359 + public void deletingLongChainNoIdWithBackreferenceNotReferencingTheRoot() { + + assertThat(createSqlGenerator(IdIdNoIdChain.class) + .createDeleteByPath(getPath("idNoIdChain.chain4.chain3.chain2.chain1.chain0", IdIdNoIdChain.class))) // + .isEqualTo( // + "DELETE FROM no_id_chain0 " // + + "WHERE no_id_chain0.no_id_chain4 IN (" // + + "SELECT no_id_chain4.x_four " // + + "FROM no_id_chain4 " // + + "WHERE no_id_chain4.id_no_id_chain IN (" // + + "SELECT id_no_id_chain.x_id " // + + "FROM id_no_id_chain " // + + "WHERE id_no_id_chain.id_id_no_id_chain = :rootId" // + + "))"); + } + @Test // DATAJDBC-340 public void noJoinForSimpleColumn() { assertThat(generateJoin("id", DummyEntity.class)).isNull(); @@ -585,4 +611,39 @@ public class SqlGeneratorUnitTests { String fourValue; Chain3 chain3; } + + static class NoIdChain0 { + String zeroValue; + } + + static class NoIdChain1 { + String oneValue; + NoIdChain0 chain0; + } + + static class NoIdChain2 { + String twoValue; + NoIdChain1 chain1; + } + + static class NoIdChain3 { + String threeValue; + NoIdChain2 chain2; + } + + static class NoIdChain4 { + @Id Long four; + String fourValue; + NoIdChain3 chain3; + } + + static class IdNoIdChain { + @Id Long id; + NoIdChain4 chain4; + } + + static class IdIdNoIdChain { + @Id Long id; + IdNoIdChain idNoIdChain; + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java index 73a3acfa..66c9c0c5 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java @@ -37,8 +37,11 @@ public class DependencyTests { classpath() // .noJars() // .including("org.springframework.data.jdbc.**") // + // the following exclusion exclude deprecated classes necessary for backward compatibility. + .excluding("org.springframework.data.jdbc.core.EntityRowMapper") // + .excluding("org.springframework.data.jdbc.core.DefaultDataAccessStrategy") // .filterClasspath("*target/classes") // exclude test code - .printOnFailure("degraph.graphml"), + .printOnFailure("degraph-jdbc.graphml"), JCheck.violationFree()); } @@ -49,6 +52,9 @@ public class DependencyTests { classpath() // // include only Spring Data related classes (for example no JDK code) .including("org.springframework.data.**") // + // the following exclusion exclude deprecated classes necessary for backward compatibility. + .excluding("org.springframework.data.jdbc.core.EntityRowMapper") // + .excluding("org.springframework.data.jdbc.core.DefaultDataAccessStrategy") // .filterClasspath(new AbstractFunction1() { @Override public Object apply(String s) { // diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedWithReferenceIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedWithReferenceIntegrationTests.java index b49b07f7..e37dbeae 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedWithReferenceIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedWithReferenceIntegrationTests.java @@ -242,6 +242,7 @@ public class JdbcRepositoryEmbeddedWithReferenceIntegrationTests { @Data private static class Embeddable { + @Column("id") DummyEntity2 dummyEntity2; 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 41971e25..61cbcd18 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 @@ -33,15 +33,14 @@ import org.assertj.core.groups.Tuple; import org.junit.Before; import org.junit.Test; import org.mockito.stubbing.Answer; - import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.SqlGeneratorSource; 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.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.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index 6d1191d8..d2d085e4 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -15,8 +15,10 @@ */ package org.springframework.data.jdbc.repository.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import lombok.Data; import java.lang.reflect.Field; @@ -32,13 +34,12 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositoriesIntegrationTests.TestConfiguration; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.CrudRepository; import org.springframework.jdbc.core.ResultSetExtractor; @@ -49,8 +50,6 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.util.ReflectionUtils; -import lombok.Data; - /** * Tests the {@link EnableJdbcRepositories} annotation. * @@ -66,7 +65,8 @@ public class EnableJdbcRepositoriesIntegrationTests { static final Field MAPPER_MAP = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, "queryMappingConfiguration"); static final Field OPERATIONS = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, "operations"); - static final Field DATA_ACCESS_STRATEGY = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, "dataAccessStrategy"); + static final Field DATA_ACCESS_STRATEGY = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, + "dataAccessStrategy"); public static final RowMapper DUMMY_ENTITY_ROW_MAPPER = mock(RowMapper.class); public static final RowMapper STRING_ROW_MAPPER = mock(RowMapper.class); public static final ResultSetExtractor INTEGER_RESULT_SET_EXTRACTOR = mock(ResultSetExtractor.class); @@ -105,13 +105,15 @@ public class EnableJdbcRepositoriesIntegrationTests { assertThat(mapping.getRowMapper(DummyEntity.class)).isEqualTo(DUMMY_ENTITY_ROW_MAPPER); } - @Test // DATAJDBC-293 + @Test // DATAJDBC-293 public void jdbcOperationsRef() { - NamedParameterJdbcOperations operations = (NamedParameterJdbcOperations) ReflectionUtils.getField(OPERATIONS, factoryBean); + NamedParameterJdbcOperations operations = (NamedParameterJdbcOperations) ReflectionUtils.getField(OPERATIONS, + factoryBean); assertThat(operations).isNotSameAs(defaultOperations).isSameAs(qualifierJdbcOperations); - DataAccessStrategy dataAccessStrategy = (DataAccessStrategy) ReflectionUtils.getField(DATA_ACCESS_STRATEGY, factoryBean); + DataAccessStrategy dataAccessStrategy = (DataAccessStrategy) ReflectionUtils.getField(DATA_ACCESS_STRATEGY, + factoryBean); assertThat(dataAccessStrategy).isNotSameAs(defaultDataAccessStrategy).isSameAs(qualifierDataAccessStrategy); } @@ -149,7 +151,8 @@ public class EnableJdbcRepositoriesIntegrationTests { } @Bean("qualifierDataAccessStrategy") - DataAccessStrategy defaultDataAccessStrategy(@Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, + DataAccessStrategy defaultDataAccessStrategy( + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, RelationalMappingContext context, JdbcConverter converter) { return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter, template); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java index 4ff45d37..b2ff30e9 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java @@ -27,6 +27,8 @@ import org.junit.Before; import org.junit.Test; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.core.DataAccessStrategy; +import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration; import org.springframework.data.jdbc.repository.query.Query; @@ -55,7 +57,7 @@ public class JdbcQueryLookupStrategyUnitTests { ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); RelationalMappingContext mappingContext = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS); - RelationalConverter converter = mock(BasicRelationalConverter.class); + JdbcConverter converter = mock(JdbcConverter.class); DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class); ProjectionFactory projectionFactory = mock(ProjectionFactory.class); RepositoryMetadata metadata; 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 a89ee08f..f4739514 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 @@ -32,8 +32,8 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcTypeFactory; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; 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 1a43ef4e..8920571a 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 @@ -28,15 +28,14 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.SqlGeneratorSource; 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.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.jdbc.repository.support.JdbcRepositoryFactory; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -63,7 +62,7 @@ public class TestConfiguration { @Bean JdbcRepositoryFactory jdbcRepositoryFactory( @Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, - RelationalConverter converter) { + JdbcConverter converter) { return new JdbcRepositoryFactory(dataAccessStrategy, context, converter, publisher, namedParameterJdbcTemplate()); } @@ -100,6 +99,7 @@ public class TestConfiguration { @Bean JdbcConverter relationalConverter(RelationalMappingContext mappingContext, CustomConversions conversions, @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template) { - return new BasicJdbcConverter(mappingContext, conversions, new DefaultJdbcTypeFactory(template.getJdbcOperations())); + return new BasicJdbcConverter(mappingContext, 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 43e6772c..a2da8d67 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 @@ -91,3 +91,37 @@ CREATE TABLE CHAIN0 CHAIN1 BIGINT, FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) ); + +CREATE TABLE NO_ID_CHAIN4 +( + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_CHAIN3 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_CHAIN1 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); 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 cccddb97..526dccd6 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 @@ -82,3 +82,37 @@ CREATE TABLE CHAIN0 CHAIN1 BIGINT, FOREIGN KEY (CHAIN1) REFERENCES CHAIN1(ONE) ); + +CREATE TABLE NO_ID_CHAIN4 +( + FOUR BIGINT AUTO_INCREMENT PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_CHAIN3 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_CHAIN1 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); 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 ff06116d..94b6aef1 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 @@ -90,4 +90,38 @@ CREATE TABLE CHAIN0 ZERO_VALUE VARCHAR(20), CHAIN1 BIGINT, FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) -); \ No newline at end of file +); + +CREATE TABLE NO_ID_CHAIN4 +( + FOUR BIGINT IDENTITY PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_CHAIN3 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_CHAIN1 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); 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 3000a7a0..526dccd6 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 @@ -81,4 +81,38 @@ CREATE TABLE CHAIN0 ZERO_VALUE VARCHAR(20), CHAIN1 BIGINT, FOREIGN KEY (CHAIN1) REFERENCES CHAIN1(ONE) -); \ No newline at end of file +); + +CREATE TABLE NO_ID_CHAIN4 +( + FOUR BIGINT AUTO_INCREMENT PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_CHAIN3 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_CHAIN1 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); 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 f996800a..c8ccc0cd 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 @@ -101,4 +101,38 @@ CREATE TABLE CHAIN0 ZERO_VALUE VARCHAR(20), CHAIN1 BIGINT, FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) -); \ No newline at end of file +); + +CREATE TABLE NO_ID_CHAIN4 +( + FOUR SERIAL PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE NO_ID_CHAIN3 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); + +CREATE TABLE NO_ID_CHAIN1 +( + 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, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedWithCollectionIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedWithCollectionIntegrationTests-hsql.sql index 92804820..0a8a9071 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedWithCollectionIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedWithCollectionIntegrationTests-hsql.sql @@ -1,2 +1,13 @@ -CREATE TABLE dummy_entity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, TEST VARCHAR(100), PREFIX_TEST VARCHAR(100)); -CREATE TABLE dummy_entity2 ( id BIGINT, ORDER_KEY BIGINT, TEST VARCHAR(100), PRIMARY KEY(id, ORDER_KEY)) +CREATE TABLE dummy_entity +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + TEST VARCHAR(100), + PREFIX_TEST VARCHAR(100) +); +CREATE TABLE dummy_entity2 +( + id BIGINT, + ORDER_KEY BIGINT, + TEST VARCHAR(100), + PRIMARY KEY (id, ORDER_KEY) +) diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedWithReferenceIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedWithReferenceIntegrationTests-hsql.sql index 14a9e03e..35561811 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedWithReferenceIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedWithReferenceIntegrationTests-hsql.sql @@ -1,2 +1,11 @@ -CREATE TABLE dummy_entity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, TEST VARCHAR(100), PREFIX_TEST VARCHAR(100)); -CREATE TABLE dummy_entity2 ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, TEST VARCHAR(100)) +CREATE TABLE dummy_entity +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + TEST VARCHAR(100), + PREFIX_TEST VARCHAR(100) +); +CREATE TABLE dummy_entity2 +( + ID BIGINT, + TEST VARCHAR(100) +) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java index 12b13916..3833475d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java @@ -24,6 +24,7 @@ import java.util.Set; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; +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; @@ -43,12 +44,10 @@ public class AggregateChange { /** Type of the aggregate root to be changed */ private final Class entityType; - + private final List> actions = new ArrayList<>(); /** Aggregate root, to which the change applies, if available */ @Nullable private T entity; - private final List> actions = new ArrayList<>(); - public AggregateChange(Kind kind, Class entityType, @Nullable T entity) { this.kind = kind; @@ -57,55 +56,11 @@ public class AggregateChange { } @SuppressWarnings("unchecked") - public void executeWith(Interpreter interpreter, RelationalMappingContext context, RelationalConverter converter) { - - RelationalPersistentEntity persistentEntity = entity != null - ? (RelationalPersistentEntity) context.getRequiredPersistentEntity(entity.getClass()) - : null; - - PersistentPropertyAccessor propertyAccessor = // - persistentEntity != null // - ? converter.getPropertyAccessor(persistentEntity, entity) // - : null; - - actions.forEach(a -> { - - a.executeWith(interpreter); - - if (a instanceof DbAction.WithGeneratedId) { - - Assert.notNull(persistentEntity, - "For statements triggering database side id generation a RelationalPersistentEntity must be provided."); - Assert.notNull(propertyAccessor, "propertyAccessor must not be null"); - - Object generatedId = ((DbAction.WithGeneratedId) a).getGeneratedId(); - - if (generatedId != null) { - - if (a instanceof DbAction.InsertRoot && a.getEntityType().equals(entityType)) { - propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), generatedId); - } else if (a instanceof DbAction.WithDependingOn) { - - setId(context, converter, propertyAccessor, (DbAction.WithDependingOn) a, generatedId); - } - } - } - }); - - if (propertyAccessor != null) { - entity = propertyAccessor.getBean(); - } - } - - public void addAction(DbAction action) { - actions.add(action); - } - - @SuppressWarnings("unchecked") - static void setId(RelationalMappingContext context, RelationalConverter converter, + static void setIdOfNonRootEntity(RelationalMappingContext context, RelationalConverter converter, PersistentPropertyAccessor propertyAccessor, DbAction.WithDependingOn action, Object generatedId) { PersistentPropertyPath propertyPathToEntity = action.getPropertyPath(); + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(context, propertyPathToEntity); RelationalPersistentProperty leafProperty = propertyPathToEntity.getRequiredLeafProperty(); @@ -130,7 +85,7 @@ public class AggregateChange { } else { throw new IllegalStateException("Can't handle " + currentPropertyValue); } - } else { + } else if (extPath.hasIdProperty()) { RelationalPersistentProperty requiredIdProperty = context .getRequiredPersistentEntity(propertyPathToEntity.getRequiredLeafProperty().getActualType()) @@ -199,6 +154,57 @@ public class AggregateChange { return intermediateAccessor; } + @SuppressWarnings("unchecked") + public void executeWith(Interpreter interpreter, RelationalMappingContext context, RelationalConverter converter) { + + RelationalPersistentEntity persistentEntity = entity != null + ? (RelationalPersistentEntity) context.getRequiredPersistentEntity(entity.getClass()) + : null; + + PersistentPropertyAccessor propertyAccessor = // + persistentEntity != null // + ? converter.getPropertyAccessor(persistentEntity, entity) // + : null; + + actions.forEach(a -> { + + a.executeWith(interpreter); + + processGeneratedId(context, converter, persistentEntity, propertyAccessor, a); + }); + + if (propertyAccessor != null) { + entity = propertyAccessor.getBean(); + } + } + + public void addAction(DbAction action) { + actions.add(action); + } + + private void processGeneratedId(RelationalMappingContext context, RelationalConverter converter, + RelationalPersistentEntity persistentEntity, PersistentPropertyAccessor propertyAccessor, DbAction a) { + + if (a instanceof DbAction.WithGeneratedId) { + + Assert.notNull(persistentEntity, + "For statements triggering database side id generation a RelationalPersistentEntity must be provided."); + Assert.notNull(propertyAccessor, "propertyAccessor must not be null"); + + Object generatedId = ((DbAction.WithGeneratedId) a).getGeneratedId(); + + if (generatedId != null) { + + if (a instanceof DbAction.InsertRoot && a.getEntityType().equals(entityType)) { + propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), generatedId); + } else if (a instanceof DbAction.WithDependingOn) { + + setIdOfNonRootEntity(context, converter, propertyAccessor, (DbAction.WithDependingOn) a, generatedId); + } + } + } + } + /** * The kind of action to be performed on an aggregate. */ diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index ecf1afd0..fd2638bd 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -180,6 +180,12 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent return collectionIdColumnName.get().orElseGet(() -> context.getNamingStrategy().getReverseColumnName(this)); } + @Override + public String getReverseColumnName(PersistentPropertyPathExtension path) { + + return collectionIdColumnName.get().orElseGet(() -> context.getNamingStrategy().getReverseColumnName(path)); + } + @Override public String getKeyColumn() { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/NamingStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/NamingStrategy.java index d0f21bab..d80dce80 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/NamingStrategy.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/NamingStrategy.java @@ -88,6 +88,11 @@ public interface NamingStrategy { return property.getOwner().getTableName(); } + default String getReverseColumnName(PersistentPropertyPathExtension path) { + + return getTableName(path.getIdDefiningParentPath().getLeafEntity().getType()); + } + /** * For a map valued reference A -> Map>X,B< this is the name of the column in the table for B holding the key of * the map. diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtension.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java similarity index 76% rename from spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtension.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java index a9a5e757..1cdccf2c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtension.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core; +package org.springframework.data.relational.core.mapping; + +import lombok.EqualsAndHashCode; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -29,13 +29,15 @@ import org.springframework.util.Assert; * @author Jens Schauder * @since 1.1 */ -class PersistentPropertyPathExtension { +@EqualsAndHashCode +public class PersistentPropertyPathExtension { private final RelationalPersistentEntity entity; private final @Nullable PersistentPropertyPath path; private final MappingContext, RelationalPersistentProperty> context; - PersistentPropertyPathExtension(MappingContext, RelationalPersistentProperty> context, + public PersistentPropertyPathExtension( + MappingContext, RelationalPersistentProperty> context, RelationalPersistentEntity entity) { Assert.notNull(context, "Context must not be null."); @@ -46,7 +48,8 @@ class PersistentPropertyPathExtension { this.path = null; } - PersistentPropertyPathExtension(MappingContext, RelationalPersistentProperty> context, + public PersistentPropertyPathExtension( + MappingContext, RelationalPersistentProperty> context, PersistentPropertyPath path) { Assert.notNull(context, "Context must not be null."); @@ -63,7 +66,7 @@ class PersistentPropertyPathExtension { * * @return if the leaf property is embedded. */ - boolean isEmbedded() { + public boolean isEmbedded() { return path != null && path.getRequiredLeafProperty().isEmbedded(); } @@ -73,7 +76,7 @@ class PersistentPropertyPathExtension { * @return the parent path. Guaranteed to be not {@literal null}. * @throws IllegalStateException when called on an empty path. */ - PersistentPropertyPathExtension getParentPath() { + public PersistentPropertyPathExtension getParentPath() { if (path == null) { throw new IllegalStateException("The parent path of a root path is not defined."); @@ -92,7 +95,7 @@ class PersistentPropertyPathExtension { * * @return {@literal true} if the path contains a multivalued element. */ - boolean isMultiValued() { + public boolean isMultiValued() { return path != null && // (path.getRequiredLeafProperty().isCollectionLike() // @@ -107,28 +110,28 @@ class PersistentPropertyPathExtension { * @return Might return {@literal null} when called on a path that does not represent an entity. */ @Nullable - RelationalPersistentEntity getLeafEntity() { + public RelationalPersistentEntity getLeafEntity() { return path == null ? entity : context.getPersistentEntity(path.getRequiredLeafProperty().getActualType()); } /** * @return {@literal true} when this is an empty path or the path references an entity. */ - boolean isEntity() { + public boolean isEntity() { return path == null || path.getRequiredLeafProperty().isEntity(); } /** * @return {@literal true} when this is references a {@link java.util.List} or {@link java.util.Map}. */ - boolean isQualified() { + public boolean isQualified() { return path != null && path.getRequiredLeafProperty().isQualified(); } /** * @return {@literal true} when this is references a {@link java.util.Collection} or an array. */ - boolean isCollectionLike() { + public boolean isCollectionLike() { return path != null && path.getRequiredLeafProperty().isCollectionLike(); } @@ -137,11 +140,11 @@ class PersistentPropertyPathExtension { * * @throws IllegalStateException when called on an empty path. */ - String getReverseColumnName() { + public String getReverseColumnName() { - Assert.state(path != null, "Path is null"); + Assert.state(path != null, "Empty paths don't have a reverse column name"); - return path.getRequiredLeafProperty().getReverseColumnName(); + return path.getRequiredLeafProperty().getReverseColumnName(this); } /** @@ -149,7 +152,7 @@ class PersistentPropertyPathExtension { * * @throws IllegalStateException when called on an empty path. */ - String getReverseColumnNameAlias() { + public String getReverseColumnNameAlias() { return prefixWithTableAlias(getReverseColumnName()); } @@ -159,7 +162,7 @@ class PersistentPropertyPathExtension { * * @throws IllegalStateException when called on an empty path. */ - String getColumnName() { + public String getColumnName() { Assert.state(path != null, "Path is null"); @@ -171,7 +174,7 @@ class PersistentPropertyPathExtension { * * @throws IllegalStateException when called on an empty path. */ - String getColumnAlias() { + public String getColumnAlias() { return prefixWithTableAlias(getColumnName()); } @@ -179,20 +182,20 @@ class PersistentPropertyPathExtension { /** * @return {@literal true} if this path represents an entity which has an Id attribute. */ - boolean hasIdProperty() { + public boolean hasIdProperty() { RelationalPersistentEntity leafEntity = getLeafEntity(); return leafEntity != null && leafEntity.hasIdProperty(); } - PersistentPropertyPathExtension getIdDefiningParentPath() { + public PersistentPropertyPathExtension getIdDefiningParentPath() { PersistentPropertyPathExtension parent = getParentPath(); if (parent.path == null) { return parent; } - if (parent.isEmbedded()) { - return getParentPath().getIdDefiningParentPath(); + if (!parent.hasIdProperty()) { + return parent.getIdDefiningParentPath(); } return parent; } @@ -202,7 +205,7 @@ class PersistentPropertyPathExtension { * * @return the name of the table. Guaranteed to be not {@literal null}. */ - String getTableName() { + public String getTableName() { return getTableOwningAncestor().getRequiredLeafEntity().getTableName(); } @@ -212,7 +215,7 @@ class PersistentPropertyPathExtension { * @return a table alias, {@literal null} if the table owning path is the empty path. */ @Nullable - String getTableAlias() { + public String getTableAlias() { PersistentPropertyPathExtension tableOwner = getTableOwningAncestor(); @@ -223,7 +226,7 @@ class PersistentPropertyPathExtension { /** * The column name of the id column of the ancestor path that represents an actual table. */ - String getIdColumnName() { + public String getIdColumnName() { return getTableOwningAncestor().getRequiredLeafEntity().getIdColumn(); } @@ -231,7 +234,7 @@ class PersistentPropertyPathExtension { * If the table owning ancestor has an id the column name of that id property is returned. Otherwise the reverse * column is returned. */ - String getEffectiveIdColumnName() { + public String getEffectiveIdColumnName() { PersistentPropertyPathExtension owner = getTableOwningAncestor(); return owner.path == null ? owner.getRequiredLeafEntity().getIdColumn() : owner.getReverseColumnName(); @@ -240,7 +243,7 @@ class PersistentPropertyPathExtension { /** * The length of the path. */ - int getLength() { + public int getLength() { return path == null ? 0 : path.getLength(); } @@ -299,4 +302,37 @@ 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/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java index 9a664d96..d4e1e33f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java @@ -56,8 +56,17 @@ public interface RelationalPersistentProperty extends PersistentProperty getOwner(); + /** + * @return + * @deprecated Use {@link #getReverseColumnName(PersistentPropertyPathExtension)} instead. + */ + @Deprecated String getReverseColumnName(); + default String getReverseColumnName(PersistentPropertyPathExtension path) { + return getReverseColumnName(); + } + @Nullable String getKeyColumn(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/package-info.java b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/package-info.java new file mode 100644 index 00000000..a930cd0b --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/package-info.java @@ -0,0 +1,4 @@ +@NonNullApi +package org.springframework.data.relational.domain; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java index 3b3b8ba2..38087754 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java @@ -67,7 +67,7 @@ public class AggregateChangeUnitTests { DbAction.Insert insert = createInsert("single", content, null); - AggregateChange.setId(context, converter, propertyAccessor, insert, id); + AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); DummyEntity result = propertyAccessor.getBean(); @@ -81,7 +81,7 @@ public class AggregateChangeUnitTests { DbAction.Insert insert = createInsert("contentSet", content, null); - AggregateChange.setId(context, converter, propertyAccessor, insert, id); + AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); DummyEntity result = propertyAccessor.getBean(); assertThat(result.contentSet).isNotNull(); @@ -95,7 +95,7 @@ public class AggregateChangeUnitTests { DbAction.Insert insert = createInsert("contentList", content, 0); - AggregateChange.setId(context, converter, propertyAccessor, insert, id); + AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); DummyEntity result = propertyAccessor.getBean(); assertThat(result.contentList).extracting(c -> c.id).containsExactlyInAnyOrder(23); @@ -108,7 +108,7 @@ public class AggregateChangeUnitTests { DbAction.Insert insert = createInsert("contentMap", content, "one"); - AggregateChange.setId(context, converter, propertyAccessor, insert, id); + AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); DummyEntity result = propertyAccessor.getBean(); assertThat(result.contentMap.entrySet()).extracting(e -> e.getKey(), e -> e.getValue().id) diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/degraph/DependencyTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/degraph/DependencyTests.java new file mode 100644 index 00000000..a1447765 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/degraph/DependencyTests.java @@ -0,0 +1,65 @@ +/* + * 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.degraph; + +import static de.schauderhaft.degraph.check.JCheck.*; +import static org.junit.Assert.*; + +import de.schauderhaft.degraph.check.JCheck; +import scala.runtime.AbstractFunction1; + +import org.junit.Test; + +/** + * Test package dependencies for violations. + * + * @author Jens Schauder + */ +public class DependencyTests { + + @Test // DATAJDBC-114 + public void cycleFree() { + + assertThat( // + classpath() // + .noJars() // + .including("org.springframework.data.relational.**") // + .filterClasspath("*target/classes") // exclude test code + .printOnFailure("degraph-relational.graphml"), + JCheck.violationFree()); + } + + @Test // DATAJDBC-220 + public void acrossModules() { + + assertThat( // + classpath() // + // include only Spring Data related classes (for example no JDK code) + .including("org.springframework.data.**") // + .filterClasspath(new AbstractFunction1() { + @Override + public Object apply(String s) { // + // only the current module + commons + return s.endsWith("target/classes") || s.contains("spring-data-commons"); + } + }) // exclude test code + .withSlicing("sub-modules", // sub-modules are defined by any of the following pattern. + "org.springframework.data.relational.(**).*", // + "org.springframework.data.(**).*") // + .printTo("degraph-across-modules.graphml"), // writes a graphml to this location + JCheck.violationFree()); + } +}