diff --git a/pom.xml b/pom.xml index b2d4b1d1ae..87736f3eb9 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-1147-fk-naming-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index db3b7ddd1a..4afe4f495b 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-1147-fk-naming-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 547ff62b8b..5830491fe6 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.0.0-SNAPSHOT + 3.0.0-1147-fk-naming-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-1147-fk-naming-SNAPSHOT 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 7a7090679a..dcafc39b41 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 @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.convert.ConverterNotFoundException; @@ -37,7 +38,6 @@ import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; -import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; import org.springframework.data.mapping.model.ParameterValueProvider; @@ -52,7 +52,6 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.util.TypeInformation; -import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -504,9 +503,7 @@ private boolean shouldCreateEmptyEmbeddedInstance(RelationalPersistentProperty p private boolean hasInstanceValues(@Nullable Object idValue) { - RelationalPersistentEntity persistentEntity = path.getLeafEntity(); - - Assert.state(persistentEntity != null, "Entity must not be null"); + RelationalPersistentEntity persistentEntity = path.getRequiredLeafEntity(); for (RelationalPersistentProperty embeddedProperty : persistentEntity) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index df8caec59e..8eb22054f0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -298,10 +298,10 @@ public Iterable findAllByPath(Identifier identifier, Assert.notNull(propertyPath, "propertyPath must not be null"); PersistentPropertyPathExtension path = new PersistentPropertyPathExtension(context, propertyPath); - Class actualType = path.getActualType(); + String findAllByProperty = sql(actualType) // - .getFindAllByProperty(identifier, path.getQualifierColumn(), path.isOrdered()); + .getFindAllByProperty(identifier, propertyPath); RowMapper rowMapper = path.isMap() ? this.getMapEntityRowMapper(path, identifier) : this.getEntityRowMapper(path, identifier); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java index 5a91abb334..7d19eb0bc8 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java @@ -37,7 +37,7 @@ class SqlContext { SqlContext(RelationalPersistentEntity entity) { this.entity = entity; - this.table = Table.create(entity.getTableName()); + this.table = Table.create(entity.getQualifiedTableName()); } Column getIdColumn() { @@ -55,7 +55,7 @@ Table getTable() { Table getTable(PersistentPropertyPathExtension path) { SqlIdentifier tableAlias = path.getTableAlias(); - Table table = Table.create(path.getTableName()); + Table table = Table.create(path.getQualifiedTableName()); return tableAlias == null ? table : table.as(tableAlias); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index ff7329a119..dd4e09aea0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -60,1046 +60,1067 @@ */ class SqlGenerator { - static final SqlIdentifier VERSION_SQL_PARAMETER = SqlIdentifier.unquoted("___oldOptimisticLockingVersion"); - static final SqlIdentifier ID_SQL_PARAMETER = SqlIdentifier.unquoted("id"); - static final SqlIdentifier IDS_SQL_PARAMETER = SqlIdentifier.unquoted("ids"); - static final SqlIdentifier ROOT_ID_PARAMETER = SqlIdentifier.unquoted("rootId"); - - private static final Pattern parameterPattern = Pattern.compile("\\W"); - private final RelationalPersistentEntity entity; - private final MappingContext, RelationalPersistentProperty> mappingContext; - private final RenderContext renderContext; - - private final SqlContext sqlContext; - private final SqlRenderer sqlRenderer; - private final Columns columns; - - private final Lazy findOneSql = Lazy.of(this::createFindOneSql); - private final Lazy findAllSql = Lazy.of(this::createFindAllSql); - private final Lazy findAllInListSql = Lazy.of(this::createFindAllInListSql); - - private final Lazy existsSql = Lazy.of(this::createExistsSql); - private final Lazy countSql = Lazy.of(this::createCountSql); - - private final Lazy updateSql = Lazy.of(this::createUpdateSql); - private final Lazy updateWithVersionSql = Lazy.of(this::createUpdateWithVersionSql); - - private final Lazy deleteByIdSql = Lazy.of(this::createDeleteByIdSql); - private final Lazy deleteByIdInSql = Lazy.of(this::createDeleteByIdInSql); - private final Lazy deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql); - private final Lazy deleteByListSql = Lazy.of(this::createDeleteByListSql); - private final QueryMapper queryMapper; - private final Dialect dialect; - - /** - * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. - * - * @param mappingContext must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param entity must not be {@literal null}. - * @param dialect must not be {@literal null}. - */ - SqlGenerator(RelationalMappingContext mappingContext, JdbcConverter converter, RelationalPersistentEntity entity, - Dialect dialect) { - - this.mappingContext = mappingContext; - this.entity = entity; - this.sqlContext = new SqlContext(entity); - this.renderContext = new RenderContextFactory(dialect).createRenderContext(); - this.sqlRenderer = SqlRenderer.create(renderContext); - this.columns = new Columns(entity, mappingContext, converter); - this.queryMapper = new QueryMapper(dialect, converter); - this.dialect = dialect; - } - - /** - * Construct an 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 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 = Table.create(parentPath.getTableName()); - Column idColumn = subSelectTable.column(parentPath.getIdColumnName()); - Column selectFilterColumn = subSelectTable.column(parentPath.getEffectiveIdColumnName()); - - Condition innerCondition; - - if (parentPath.getLength() == 1) { // if the parent is the root of the path - - // apply the rootCondition - innerCondition = rootCondition.apply(selectFilterColumn); - } else { - - // otherwise, we need another layer of subselect - innerCondition = getSubselectCondition(parentPath, rootCondition, selectFilterColumn); - } - - Select select = Select.builder() // - .select(idColumn) // - .from(subSelectTable) // - .where(innerCondition).build(); - - return filterColumn.in(select); - } - - private BindMarker getBindMarker(SqlIdentifier columnName) { - return SQL.bindMarker(":" + parameterPattern.matcher(renderReference(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. - * - * @return a SQL statement. Guaranteed to be not {@code null}. - */ - String getFindAllInList() { - return findAllInListSql.get(); - } - - /** - * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. - * - * @return a SQL statement. Guaranteed to be not {@code null}. - */ - String getFindAll() { - return findAllSql.get(); - } - - /** - * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships, - * sorted by the given parameter. - * - * @return a SQL statement. Guaranteed to be not {@code null}. - */ - String getFindAll(Sort sort) { - return render(selectBuilder(Collections.emptyList(), sort, Pageable.unpaged()).build()); - } - - /** - * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships, - * paged and sorted by the given parameter. - * - * @return a SQL statement. Guaranteed to be not {@code null}. - */ - String getFindAll(Pageable pageable) { - return render(selectBuilder(Collections.emptyList(), pageable.getSort(), pageable).build()); - } - - /** - * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. - * Results are limited to those rows referencing some other entity using the column specified by - * {@literal columnName}. This is used to select values for a complex property ({@link Set}, {@link Map} ...) based on - * a referencing entity. - * - * @param parentIdentifier name of the column of the FK back to the referencing entity. - * @param keyColumn if the property is of type {@link Map} this column contains the map key. - * @param ordered whether the SQL statement should include an ORDER BY for the keyColumn. If this is {@code true}, the - * keyColumn must not be {@code null}. - * @return a SQL String. - */ - String getFindAllByProperty(Identifier parentIdentifier, @Nullable SqlIdentifier keyColumn, boolean ordered) { - - Assert.isTrue(keyColumn != null || !ordered, - "If the SQL statement should be ordered a keyColumn to order by must be provided"); - - Table table = getTable(); - - SelectBuilder.SelectWhere builder = selectBuilder( // - keyColumn == null // - ? Collections.emptyList() // - : Collections.singleton(keyColumn) // - ); - - Condition condition = buildConditionForBackReference(parentIdentifier, table); - SelectBuilder.SelectWhereAndOr withWhereClause = builder.where(condition); - - Select select = ordered // - ? withWhereClause.orderBy(table.column(keyColumn).as(keyColumn)).build() // - : withWhereClause.build(); - - return render(select); - } - - private Condition buildConditionForBackReference(Identifier parentIdentifier, Table table) { - - Condition condition = null; - for (SqlIdentifier backReferenceColumn : parentIdentifier.toMap().keySet()) { - - Condition newCondition = table.column(backReferenceColumn).isEqualTo(getBindMarker(backReferenceColumn)); - condition = condition == null ? newCondition : condition.and(newCondition); - } - - Assert.state(condition != null, "We need at least one condition"); - - return condition; - } - - /** - * Create a {@code SELECT COUNT(id) FROM … WHERE :id = …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getExists() { - return existsSql.get(); - } - - /** - * Create a {@code SELECT … FROM … WHERE :id = …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getFindOne() { - return findOneSql.get(); - } - - /** - * Create a {@code SELECT count(id) FROM … WHERE :id = … (LOCK CLAUSE)} statement. - * - * @param lockMode Lock clause mode. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getAcquireLockById(LockMode lockMode) { - return this.createAcquireLockById(lockMode); - } - - /** - * Create a {@code SELECT count(id) FROM … (LOCK CLAUSE)} statement. - * - * @param lockMode Lock clause mode. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getAcquireLockAll(LockMode lockMode) { - return this.createAcquireLockAll(lockMode); - } - - /** - * Create a {@code INSERT INTO … (…) VALUES(…)} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getInsert(Set additionalColumns) { - return createInsertSql(additionalColumns); - } - - /** - * Create a {@code UPDATE … SET …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getUpdate() { - return updateSql.get(); - } - - /** - * Create a {@code UPDATE … SET … WHERE ID = :id and VERSION_COLUMN = :___oldOptimisticLockingVersion } statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getUpdateWithVersion() { - return updateWithVersionSql.get(); - } - - /** - * Create a {@code SELECT COUNT(*) FROM …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getCount() { - return countSql.get(); - } - - /** - * Create a {@code DELETE FROM … WHERE :id = …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getDeleteById() { - return deleteByIdSql.get(); - } - - /** - * Create a {@code DELETE FROM … WHERE :id IN …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getDeleteByIdIn() { - return deleteByIdInSql.get(); - } - - /** - * Create a {@code DELETE FROM … WHERE :id = … and :___oldOptimisticLockingVersion = ...} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getDeleteByIdAndVersion() { - return deleteByIdAndVersionSql.get(); - } - - /** - * Create a {@code DELETE FROM … WHERE :ids in (…)} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getDeleteByList() { - return deleteByListSql.get(); - } - - /** - * Create a {@code DELETE} query and optionally filter by {@link PersistentPropertyPath}. - * - * @param path can be {@literal null}. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String createDeleteAllSql(@Nullable PersistentPropertyPath path) { - - Table table = getTable(); - - DeleteBuilder.DeleteWhere deleteAll = Delete.builder().from(table); - - if (path == null) { - return render(deleteAll.build()); - } - - return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), Column::isNotNull); - } - - /** - * Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code =} - * operator. - * - * @param path must not be {@literal null}. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String createDeleteByPath(PersistentPropertyPath path) { - return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), - filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); - } - - /** - * Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code IN} - * operator. - * - * @param path must not be {@literal null}. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String createDeleteInByPath(PersistentPropertyPath path) { - - return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), - filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER))); - } - - private String createFindOneSql() { - - Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); - - return render(select); - } - - private String createAcquireLockById(LockMode lockMode) { - - Table table = this.getTable(); - - Select select = StatementBuilder // - .select(getIdColumn()) // - .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .lock(lockMode) // - .build(); - - return render(select); - } - - private String createAcquireLockAll(LockMode lockMode) { - - Table table = this.getTable(); - - Select select = StatementBuilder // - .select(getIdColumn()) // - .from(table) // - .lock(lockMode) // - .build(); - - return render(select); - } - - private String createFindAllSql() { - return render(selectBuilder().build()); - } - - private SelectBuilder.SelectWhere selectBuilder() { - return selectBuilder(Collections.emptyList()); - } - - private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns) { + static final SqlIdentifier VERSION_SQL_PARAMETER = SqlIdentifier.unquoted("___oldOptimisticLockingVersion"); + static final SqlIdentifier ID_SQL_PARAMETER = SqlIdentifier.unquoted("id"); + static final SqlIdentifier IDS_SQL_PARAMETER = SqlIdentifier.unquoted("ids"); + static final SqlIdentifier ROOT_ID_PARAMETER = SqlIdentifier.unquoted("rootId"); + + private static final Pattern parameterPattern = Pattern.compile("\\W"); + private final RelationalPersistentEntity entity; + private final MappingContext, RelationalPersistentProperty> mappingContext; + private final RenderContext renderContext; + + private final SqlContext sqlContext; + private final SqlRenderer sqlRenderer; + private final Columns columns; + + private final Lazy findOneSql = Lazy.of(this::createFindOneSql); + private final Lazy findAllSql = Lazy.of(this::createFindAllSql); + private final Lazy findAllInListSql = Lazy.of(this::createFindAllInListSql); + + private final Lazy existsSql = Lazy.of(this::createExistsSql); + private final Lazy countSql = Lazy.of(this::createCountSql); + + private final Lazy updateSql = Lazy.of(this::createUpdateSql); + private final Lazy updateWithVersionSql = Lazy.of(this::createUpdateWithVersionSql); + + private final Lazy deleteByIdSql = Lazy.of(this::createDeleteByIdSql); + private final Lazy deleteByIdInSql = Lazy.of(this::createDeleteByIdInSql); + private final Lazy deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql); + private final Lazy deleteByListSql = Lazy.of(this::createDeleteByListSql); + private final QueryMapper queryMapper; + private final Dialect dialect; + + /** + * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. + * + * @param mappingContext must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param entity must not be {@literal null}. + * @param dialect must not be {@literal null}. + */ + SqlGenerator(RelationalMappingContext mappingContext, JdbcConverter converter, RelationalPersistentEntity entity, + Dialect dialect) { + + this.mappingContext = mappingContext; + this.entity = entity; + this.sqlContext = new SqlContext(entity); + this.renderContext = new RenderContextFactory(dialect).createRenderContext(); + this.sqlRenderer = SqlRenderer.create(renderContext); + this.columns = new Columns(entity, mappingContext, converter); + this.queryMapper = new QueryMapper(dialect, converter); + this.dialect = dialect; + } + + /** + * Construct an 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 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 = Table.create(parentPath.getQualifiedTableName()); + Column idColumn = subSelectTable.column(parentPath.getIdColumnName()); + Column selectFilterColumn = subSelectTable.column(parentPath.getEffectiveIdColumnName()); + + Condition innerCondition; + + if (parentPath.getLength() == 1) { // if the parent is the root of the path + + // apply the rootCondition + innerCondition = rootCondition.apply(selectFilterColumn); + } else { + + // otherwise, we need another layer of subselect + innerCondition = getSubselectCondition(parentPath, rootCondition, selectFilterColumn); + } + + Select select = Select.builder() // + .select(idColumn) // + .from(subSelectTable) // + .where(innerCondition).build(); + + return filterColumn.in(select); + } + + private BindMarker getBindMarker(SqlIdentifier columnName) { + return SQL.bindMarker(":" + parameterPattern.matcher(renderReference(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. + * + * @return a SQL statement. Guaranteed to be not {@code null}. + */ + String getFindAllInList() { + return findAllInListSql.get(); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. + * + * @return a SQL statement. Guaranteed to be not {@code null}. + */ + String getFindAll() { + return findAllSql.get(); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships, + * sorted by the given parameter. + * + * @return a SQL statement. Guaranteed to be not {@code null}. + */ + String getFindAll(Sort sort) { + return render(selectBuilder(Collections.emptyList(), sort, Pageable.unpaged()).build()); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships, + * paged and sorted by the given parameter. + * + * @return a SQL statement. Guaranteed to be not {@code null}. + */ + String getFindAll(Pageable pageable) { + return render(selectBuilder(Collections.emptyList(), pageable.getSort(), pageable).build()); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. + * Results are limited to those rows referencing some parent entity. This is used to select values for a complex + * property ({@link Set}, {@link Map} ...) based on a referencing entity. + * + * @param parentIdentifier name of the column of the FK back to the referencing entity. + * @param propertyPath used to determine if the property is ordered and if there is a key column. + * @return a SQL String. + * @since 3.0 + */ + String getFindAllByProperty(Identifier parentIdentifier, + PersistentPropertyPath propertyPath) { + + Assert.notNull(parentIdentifier, "identifier must not be null"); + Assert.notNull(propertyPath, "propertyPath must not be null"); + + PersistentPropertyPathExtension path = new PersistentPropertyPathExtension(mappingContext, propertyPath); + + return getFindAllByProperty(parentIdentifier, path.getQualifierColumn(), path.isOrdered()); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. + * Results are limited to those rows referencing some other entity using the column specified by + * {@literal columnName}. This is used to select values for a complex property ({@link Set}, {@link Map} ...) based on + * a referencing entity. + * + * @param parentIdentifier name of the column of the FK back to the referencing entity. + * @param keyColumn if the property is of type {@link Map} this column contains the map key. + * @param ordered whether the SQL statement should include an ORDER BY for the keyColumn. If this is {@code true}, the + * keyColumn must not be {@code null}. + * @return a SQL String. + */ + String getFindAllByProperty(Identifier parentIdentifier, @Nullable SqlIdentifier keyColumn, boolean ordered) { + + Assert.isTrue(keyColumn != null || !ordered, + "If the SQL statement should be ordered a keyColumn to order by must be provided"); + + Table table = getTable(); + + SelectBuilder.SelectWhere builder = selectBuilder( // + keyColumn == null // + ? Collections.emptyList() // + : Collections.singleton(keyColumn) // + ); + + Condition condition = buildConditionForBackReference(parentIdentifier, table); + SelectBuilder.SelectWhereAndOr withWhereClause = builder.where(condition); + + Select select = ordered // + ? withWhereClause.orderBy(table.column(keyColumn).as(keyColumn)).build() // + : withWhereClause.build(); + + return render(select); + } + + private Condition buildConditionForBackReference(Identifier parentIdentifier, Table table) { + + Condition condition = null; + for (SqlIdentifier backReferenceColumn : parentIdentifier.toMap().keySet()) { + + Condition newCondition = table.column(backReferenceColumn).isEqualTo(getBindMarker(backReferenceColumn)); + condition = condition == null ? newCondition : condition.and(newCondition); + } + + Assert.state(condition != null, "We need at least one condition"); + + return condition; + } + + /** + * Create a {@code SELECT COUNT(id) FROM … WHERE :id = …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getExists() { + return existsSql.get(); + } + + /** + * Create a {@code SELECT … FROM … WHERE :id = …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getFindOne() { + return findOneSql.get(); + } + + /** + * Create a {@code SELECT count(id) FROM … WHERE :id = … (LOCK CLAUSE)} statement. + * + * @param lockMode Lock clause mode. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getAcquireLockById(LockMode lockMode) { + return this.createAcquireLockById(lockMode); + } + + /** + * Create a {@code SELECT count(id) FROM … (LOCK CLAUSE)} statement. + * + * @param lockMode Lock clause mode. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getAcquireLockAll(LockMode lockMode) { + return this.createAcquireLockAll(lockMode); + } + + /** + * Create a {@code INSERT INTO … (…) VALUES(…)} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getInsert(Set additionalColumns) { + return createInsertSql(additionalColumns); + } + + /** + * Create a {@code UPDATE … SET …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getUpdate() { + return updateSql.get(); + } + + /** + * Create a {@code UPDATE … SET … WHERE ID = :id and VERSION_COLUMN = :___oldOptimisticLockingVersion } statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getUpdateWithVersion() { + return updateWithVersionSql.get(); + } + + /** + * Create a {@code SELECT COUNT(*) FROM …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getCount() { + return countSql.get(); + } + + /** + * Create a {@code DELETE FROM … WHERE :id = …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getDeleteById() { + return deleteByIdSql.get(); + } + + /** + * Create a {@code DELETE FROM … WHERE :id IN …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getDeleteByIdIn() { + return deleteByIdInSql.get(); + } + + /** + * Create a {@code DELETE FROM … WHERE :id = … and :___oldOptimisticLockingVersion = ...} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getDeleteByIdAndVersion() { + return deleteByIdAndVersionSql.get(); + } + + /** + * Create a {@code DELETE FROM … WHERE :ids in (…)} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getDeleteByList() { + return deleteByListSql.get(); + } + + /** + * Create a {@code DELETE} query and optionally filter by {@link PersistentPropertyPath}. + * + * @param path can be {@literal null}. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String createDeleteAllSql(@Nullable PersistentPropertyPath path) { + + Table table = getTable(); + + DeleteBuilder.DeleteWhere deleteAll = Delete.builder().from(table); + + if (path == null) { + return render(deleteAll.build()); + } + + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), Column::isNotNull); + } + + /** + * Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code =} + * operator. + * + * @param path must not be {@literal null}. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String createDeleteByPath(PersistentPropertyPath path) { + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), + filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); + } + + /** + * Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code IN} + * operator. + * + * @param path must not be {@literal null}. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String createDeleteInByPath(PersistentPropertyPath path) { + + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), + filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER))); + } + + private String createFindOneSql() { + + Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .build(); + + return render(select); + } + + private String createAcquireLockById(LockMode lockMode) { + + Table table = this.getTable(); + + Select select = StatementBuilder // + .select(getIdColumn()) // + .from(table) // + .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .lock(lockMode) // + .build(); + + return render(select); + } + + private String createAcquireLockAll(LockMode lockMode) { + + Table table = this.getTable(); + + Select select = StatementBuilder // + .select(getIdColumn()) // + .from(table) // + .lock(lockMode) // + .build(); + + return render(select); + } + + private String createFindAllSql() { + return render(selectBuilder().build()); + } + + private SelectBuilder.SelectWhere selectBuilder() { + return selectBuilder(Collections.emptyList()); + } + + private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns) { - Table table = getTable(); + Table table = getTable(); - List columnExpressions = new ArrayList<>(); + List columnExpressions = new ArrayList<>(); - List joinTables = new ArrayList<>(); - for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { + List joinTables = new ArrayList<>(); + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), p -> true)) { - PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - joinTables.add(join); - } + // add a join if necessary + Join join = getJoin(extPath); + if (join != null) { + joinTables.add(join); + } - Column column = getColumn(extPath); - if (column != null) { - columnExpressions.add(column); - } - } + Column column = getColumn(extPath); + if (column != null) { + columnExpressions.add(column); + } + } - for (SqlIdentifier keyColumn : keyColumns) { - columnExpressions.add(table.column(keyColumn).as(keyColumn)); - } + for (SqlIdentifier keyColumn : keyColumns) { + columnExpressions.add(table.column(keyColumn).as(keyColumn)); + } - SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); - SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); + SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); + SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); - for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } + for (Join join : joinTables) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + } - return (SelectBuilder.SelectWhere) baseSelect; - } + return (SelectBuilder.SelectWhere) baseSelect; + } - private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, - Pageable pageable) { + private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, + Pageable pageable) { - SelectBuilder.SelectOrdered sortable = this.selectBuilder(keyColumns); - sortable = applyPagination(pageable, sortable); - return sortable.orderBy(extractOrderByFields(sort)); + SelectBuilder.SelectOrdered sortable = this.selectBuilder(keyColumns); + sortable = applyPagination(pageable, sortable); + return sortable.orderBy(extractOrderByFields(sort)); - } + } - private SelectBuilder.SelectOrdered applyPagination(Pageable pageable, SelectBuilder.SelectOrdered select) { + private SelectBuilder.SelectOrdered applyPagination(Pageable pageable, SelectBuilder.SelectOrdered select) { - if (!pageable.isPaged()) { - return select; - } + if (!pageable.isPaged()) { + return select; + } - Assert.isTrue(select instanceof SelectBuilder.SelectLimitOffset, - () -> String.format("Can't apply limit clause to statement of type %s", select.getClass())); + Assert.isTrue(select instanceof SelectBuilder.SelectLimitOffset, + () -> String.format("Can't apply limit clause to statement of type %s", select.getClass())); - SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) select; - SelectBuilder.SelectLimitOffset limitResult = limitable.limitOffset(pageable.getPageSize(), pageable.getOffset()); + SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) select; + SelectBuilder.SelectLimitOffset limitResult = limitable.limitOffset(pageable.getPageSize(), pageable.getOffset()); - Assert.state(limitResult instanceof SelectBuilder.SelectOrdered, String.format( - "The result of applying the limit-clause must be of type SelectOrdered in order to apply the order-by-clause but is of type %s", - select.getClass())); + Assert.state(limitResult instanceof SelectBuilder.SelectOrdered, String.format( + "The result of applying the limit-clause must be of type SelectOrdered in order to apply the order-by-clause but is of type %s", + select.getClass())); - return (SelectBuilder.SelectOrdered) limitResult; - } + return (SelectBuilder.SelectOrdered) limitResult; + } - /** - * Create a {@link Column} for {@link PersistentPropertyPathExtension}. - * - * @param path the path to the column in question. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - @Nullable - Column getColumn(PersistentPropertyPathExtension path) { + /** + * Create a {@link Column} for {@link PersistentPropertyPathExtension}. + * + * @param path the path to the column in question. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + @Nullable + Column getColumn(PersistentPropertyPathExtension path) { - // an embedded itself doesn't give a column, its members will though. - // if there is a collection or map on the path it won't get selected at all, but it will get loaded with a separate - // select - // only the parent path is considered in order to handle arrays that get stored as BINARY properly - if (path.isEmbedded() || path.getParentPath().isMultiValued()) { - return null; - } + // an embedded itself doesn't give a column, its members will though. + // if there is a collection or map on the path it won't get selected at all, but it will get loaded with a separate + // select + // only the parent path is considered in order to handle arrays that get stored as BINARY properly + if (path.isEmbedded() || path.getParentPath().isMultiValued()) { + return null; + } - if (path.isEntity()) { + if (path.isEntity()) { - // Simple entities without id include there backreference as a synthetic id in order to distinguish null entities - // from entities with only null values. + // Simple entities without id include there backreference as a synthetic id in order to distinguish null entities + // from entities with only null values. - if (path.isQualified() // - || path.isCollectionLike() // - || path.hasIdProperty() // - ) { - return null; - } + if (path.isQualified() // + || path.isCollectionLike() // + || path.hasIdProperty() // + ) { + return null; + } - return sqlContext.getReverseColumn(path); - } + return sqlContext.getReverseColumn(path); + } - return sqlContext.getColumn(path); - } + return sqlContext.getColumn(path); + } - @Nullable - Join getJoin(PersistentPropertyPathExtension path) { + @Nullable + Join getJoin(PersistentPropertyPathExtension path) { - if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) { - return null; - } + if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) { + return null; + } - Table currentTable = sqlContext.getTable(path); + Table currentTable = sqlContext.getTable(path); - PersistentPropertyPathExtension idDefiningParentPath = path.getIdDefiningParentPath(); - Table parentTable = sqlContext.getTable(idDefiningParentPath); + PersistentPropertyPathExtension idDefiningParentPath = path.getIdDefiningParentPath(); + Table parentTable = sqlContext.getTable(idDefiningParentPath); - return new Join( // - currentTable, // - currentTable.column(path.getReverseColumnName()), // - parentTable.column(idDefiningParentPath.getIdColumnName()) // - ); - } + return new Join( // + currentTable, // + currentTable.column(path.getReverseColumnName()), // + parentTable.column(idDefiningParentPath.getIdColumnName()) // + ); + } - private String createFindAllInListSql() { + private String createFindAllInListSql() { - Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); + Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); - return render(select); - } + return render(select); + } - private String createExistsSql() { + private String createExistsSql() { - Table table = getTable(); + Table table = getTable(); - Select select = StatementBuilder // - .select(Functions.count(getIdColumn())) // - .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); + Select select = StatementBuilder // + .select(Functions.count(getIdColumn())) // + .from(table) // + .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .build(); - return render(select); - } + return render(select); + } - private String createCountSql() { + private String createCountSql() { - Table table = getTable(); + Table table = getTable(); - Select select = StatementBuilder // - .select(Functions.count(Expressions.asterisk())) // - .from(table) // - .build(); + Select select = StatementBuilder // + .select(Functions.count(Expressions.asterisk())) // + .from(table) // + .build(); - return render(select); - } + return render(select); + } - private String createInsertSql(Set additionalColumns) { + private String createInsertSql(Set additionalColumns) { - Table table = getTable(); + Table table = getTable(); - Set columnNamesForInsert = new TreeSet<>(Comparator.comparing(SqlIdentifier::getReference)); - columnNamesForInsert.addAll(columns.getInsertableColumns()); - columnNamesForInsert.addAll(additionalColumns); + Set columnNamesForInsert = new TreeSet<>(Comparator.comparing(SqlIdentifier::getReference)); + columnNamesForInsert.addAll(columns.getInsertableColumns()); + columnNamesForInsert.addAll(additionalColumns); - InsertBuilder.InsertIntoColumnsAndValuesWithBuild insert = Insert.builder().into(table); + InsertBuilder.InsertIntoColumnsAndValuesWithBuild insert = Insert.builder().into(table); - for (SqlIdentifier cn : columnNamesForInsert) { - insert = insert.column(table.column(cn)); - } + for (SqlIdentifier cn : columnNamesForInsert) { + insert = insert.column(table.column(cn)); + } - if (columnNamesForInsert.isEmpty()) { - return render(insert.build()); - } + if (columnNamesForInsert.isEmpty()) { + return render(insert.build()); + } - InsertBuilder.InsertValuesWithBuild insertWithValues = null; - for (SqlIdentifier cn : columnNamesForInsert) { - insertWithValues = (insertWithValues == null ? insert : insertWithValues).values(getBindMarker(cn)); - } + InsertBuilder.InsertValuesWithBuild insertWithValues = null; + for (SqlIdentifier cn : columnNamesForInsert) { + insertWithValues = (insertWithValues == null ? insert : insertWithValues).values(getBindMarker(cn)); + } - return render(insertWithValues.build()); - } + return render(insertWithValues.build()); + } - private String createUpdateSql() { - return render(createBaseUpdate().build()); - } + private String createUpdateSql() { + return render(createBaseUpdate().build()); + } - private String createUpdateWithVersionSql() { + private String createUpdateWithVersionSql() { - Update update = createBaseUpdate() // - .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // - .build(); + Update update = createBaseUpdate() // + .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // + .build(); - return render(update); - } + return render(update); + } - private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() { + private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() { - Table table = getTable(); + Table table = getTable(); - List assignments = columns.getUpdatableColumns() // - .stream() // - .map(columnName -> Assignments.value( // - table.column(columnName), // - getBindMarker(columnName))) // - .collect(Collectors.toList()); + List assignments = columns.getUpdatableColumns() // + .stream() // + .map(columnName -> Assignments.value( // + table.column(columnName), // + getBindMarker(columnName))) // + .collect(Collectors.toList()); - return Update.builder() // - .table(table) // - .set(assignments) // - .where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))); - } + return Update.builder() // + .table(table) // + .set(assignments) // + .where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))); + } - private String createDeleteByIdSql() { - return render(createBaseDeleteById(getTable()).build()); - } + private String createDeleteByIdSql() { + return render(createBaseDeleteById(getTable()).build()); + } - private String createDeleteByIdInSql() { - return render(createBaseDeleteByIdIn(getTable()).build()); - } + private String createDeleteByIdInSql() { + return render(createBaseDeleteByIdIn(getTable()).build()); + } - private String createDeleteByIdAndVersionSql() { + private String createDeleteByIdAndVersionSql() { - Delete delete = createBaseDeleteById(getTable()) // - .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // - .build(); + Delete delete = createBaseDeleteById(getTable()) // + .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // + .build(); - return render(delete); - } + return render(delete); + } - private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { + private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { - return Delete.builder().from(table) - .where(getIdColumn().isEqualTo(SQL.bindMarker(":" + renderReference(ID_SQL_PARAMETER)))); - } + return Delete.builder().from(table) + .where(getIdColumn().isEqualTo(SQL.bindMarker(":" + renderReference(ID_SQL_PARAMETER)))); + } - private DeleteBuilder.DeleteWhereAndOr createBaseDeleteByIdIn(Table table) { + private DeleteBuilder.DeleteWhereAndOr createBaseDeleteByIdIn(Table table) { - return Delete.builder().from(table) - .where(getIdColumn().in(SQL.bindMarker(":" + renderReference(IDS_SQL_PARAMETER)))); - } + return Delete.builder().from(table) + .where(getIdColumn().in(SQL.bindMarker(":" + renderReference(IDS_SQL_PARAMETER)))); + } - private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path, - Function rootCondition) { + private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path, + Function rootCondition) { - Table table = Table.create(path.getTableName()); + Table table = Table.create(path.getQualifiedTableName()); - DeleteBuilder.DeleteWhere builder = Delete.builder() // - .from(table); - Delete delete; + DeleteBuilder.DeleteWhere builder = Delete.builder() // + .from(table); + Delete delete; - Column filterColumn = table.column(path.getReverseColumnName()); + Column filterColumn = table.column(path.getReverseColumnName()); - if (path.getLength() == 1) { + if (path.getLength() == 1) { - delete = builder // - .where(rootCondition.apply(filterColumn)) // - .build(); - } else { + delete = builder // + .where(rootCondition.apply(filterColumn)) // + .build(); + } else { - Condition condition = getSubselectCondition(path, rootCondition, filterColumn); - delete = builder.where(condition).build(); - } + Condition condition = getSubselectCondition(path, rootCondition, filterColumn); + delete = builder.where(condition).build(); + } - return render(delete); - } + return render(delete); + } - private String createDeleteByListSql() { + private String createDeleteByListSql() { - Table table = getTable(); + Table table = getTable(); - Delete delete = Delete.builder() // - .from(table) // - .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))) // - .build(); + Delete delete = Delete.builder() // + .from(table) // + .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))) // + .build(); - return render(delete); - } + return render(delete); + } - private String render(Select select) { - return this.sqlRenderer.render(select); - } + private String render(Select select) { + return this.sqlRenderer.render(select); + } - private String render(Insert insert) { - return this.sqlRenderer.render(insert); - } + private String render(Insert insert) { + return this.sqlRenderer.render(insert); + } - private String render(Update update) { - return this.sqlRenderer.render(update); - } + private String render(Update update) { + return this.sqlRenderer.render(update); + } - private String render(Delete delete) { - return this.sqlRenderer.render(delete); - } + private String render(Delete delete) { + return this.sqlRenderer.render(delete); + } - private Table getTable() { - return sqlContext.getTable(); - } + private Table getTable() { + return sqlContext.getTable(); + } - private Column getIdColumn() { - return sqlContext.getIdColumn(); - } + private Column getIdColumn() { + return sqlContext.getIdColumn(); + } - private Column getVersionColumn() { - return sqlContext.getVersionColumn(); - } + private Column getVersionColumn() { + return sqlContext.getVersionColumn(); + } - private String renderReference(SqlIdentifier identifier) { - return identifier.getReference(renderContext.getIdentifierProcessing()); - } + private String renderReference(SqlIdentifier identifier) { + return identifier.getReference(renderContext.getIdentifierProcessing()); + } - private List extractOrderByFields(Sort sort) { + private List extractOrderByFields(Sort sort) { - return sort.stream() // - .map(this::orderToOrderByField) // - .collect(Collectors.toList()); - } + return sort.stream() // + .map(this::orderToOrderByField) // + .collect(Collectors.toList()); + } - private OrderByField orderToOrderByField(Sort.Order order) { + private OrderByField orderToOrderByField(Sort.Order order) { - SqlIdentifier columnName = this.entity.getRequiredPersistentProperty(order.getProperty()).getColumnName(); - Column column = Column.create(columnName, this.getTable()); - return OrderByField.from(column, order.getDirection()).withNullHandling(order.getNullHandling()); - } + SqlIdentifier columnName = this.entity.getRequiredPersistentProperty(order.getProperty()).getColumnName(); + Column column = Column.create(columnName, this.getTable()); + return OrderByField.from(column, order.getDirection()).withNullHandling(order.getNullHandling()); + } - /** - * Constructs a single sql query that performs select based on the provided query. Additional the bindings for the - * where clause are stored after execution into the parameterSource - * - * @param query the query to base the select on. Must not be null - * @param parameterSource the source for holding the bindings - * @return a non null query string. - */ - public String selectByQuery(Query query, MapSqlParameterSource parameterSource) { + /** + * Constructs a single sql query that performs select based on the provided query. Additional the bindings for the + * where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null + * @param parameterSource the source for holding the bindings + * @return a non null query string. + */ + public String selectByQuery(Query query, MapSqlParameterSource parameterSource) { - Assert.notNull(parameterSource, "parameterSource must not be null"); + Assert.notNull(parameterSource, "parameterSource must not be null"); - SelectBuilder.SelectWhere selectBuilder = selectBuilder(); - - Select select = applyQueryOnSelect(query, parameterSource, selectBuilder) // - .build(); - - return render(select); - } - - /** - * Constructs a single sql query that performs select based on the provided query and pagination information. - * Additional the bindings for the where clause are stored after execution into the parameterSource - * - * @param query the query to base the select on. Must not be null. - * @param pageable the pageable to perform on the select. - * @param parameterSource the source for holding the bindings. - * @return a non null query string. - */ - public String selectByQuery(Query query, MapSqlParameterSource parameterSource, Pageable pageable) { - - Assert.notNull(parameterSource, "parameterSource must not be null"); - - SelectBuilder.SelectWhere selectBuilder = selectBuilder(); - - // first apply query and then pagination. This means possible query sorting and limiting might be overwritten by the - // pagination. This is desired. - SelectBuilder.SelectOrdered selectOrdered = applyQueryOnSelect(query, parameterSource, selectBuilder); - selectOrdered = applyPagination(pageable, selectOrdered); - selectOrdered = selectOrdered.orderBy(extractOrderByFields(pageable.getSort())); - - Select select = selectOrdered.build(); - return render(select); - } - - /** - * Constructs a single sql query that performs select count based on the provided query for checking existence. - * Additional the bindings for the where clause are stored after execution into the parameterSource - * - * @param query the query to base the select on. Must not be null - * @param parameterSource the source for holding the bindings - * @return a non null query string. - */ - public String existsByQuery(Query query, MapSqlParameterSource parameterSource) { - - SelectBuilder.SelectJoin baseSelect = getExistsSelect(); - - Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // - .build(); - - return render(select); - } - - /** - * Constructs a single sql query that performs select count based on the provided query. Additional the bindings for - * the where clause are stored after execution into the parameterSource - * - * @param query the query to base the select on. Must not be null - * @param parameterSource the source for holding the bindings - * @return a non null query string. - */ - public String countByQuery(Query query, MapSqlParameterSource parameterSource) { - - Expression countExpression = Expressions.just("1"); - SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(countExpression); - - Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // - .build(); - - return render(select); - } - - /** - * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a - * COUNT(...) where the countExpressions are the parameters of the count. - * - * @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the - * columns and has only a count in the projection of the select. - */ - private SelectBuilder.SelectJoin getExistsSelect() { - - Table table = getTable(); - - SelectBuilder.SelectJoin baseSelect = StatementBuilder // - .select(dialect.getExistsFunction()) // - .from(table); - - // add possible joins - for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { - - PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); - - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } - } - return baseSelect; - } - - /** - * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a - * COUNT(...) where the countExpressions are the parameters of the count. - * - * @param countExpressions the expression to use as count parameter. - * @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the - * columns and has only a count in the projection of the select. - */ - private SelectBuilder.SelectJoin getSelectCountWithExpression(Expression... countExpressions) { - - Assert.notNull(countExpressions, "countExpressions must not be null"); - Assert.state(countExpressions.length >= 1, "countExpressions must contain at least one expression"); - - Table table = getTable(); - - SelectBuilder.SelectJoin baseSelect = StatementBuilder // - .select(Functions.count(countExpressions)) // - .from(table); - - // add possible joins - for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { - - PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); - - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } - } - return baseSelect; - } - - private SelectBuilder.SelectOrdered applyQueryOnSelect(Query query, MapSqlParameterSource parameterSource, - SelectBuilder.SelectWhere selectBuilder) { - - Table table = Table.create(this.entity.getTableName()); - - SelectBuilder.SelectOrdered selectOrdered = query // - .getCriteria() // - .map(item -> this.applyCriteria(item, selectBuilder, parameterSource, table)) // - .orElse(selectBuilder); - - if (query.isSorted()) { - List sort = this.queryMapper.getMappedSort(table, query.getSort(), entity); - selectOrdered = selectBuilder.orderBy(sort); - } - - SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) selectOrdered; - - if (query.getLimit() > 0) { - limitable = limitable.limit(query.getLimit()); - } - - if (query.getOffset() > 0) { - limitable = limitable.offset(query.getOffset()); - } - return (SelectBuilder.SelectOrdered) limitable; - } - - SelectBuilder.SelectOrdered applyCriteria(@Nullable CriteriaDefinition criteria, - SelectBuilder.SelectWhere whereBuilder, MapSqlParameterSource parameterSource, Table table) { - - return criteria == null || criteria.isEmpty() // Check for null and empty criteria - ? whereBuilder // - : whereBuilder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity)); - } - - /** - * Value object representing a {@code JOIN} association. - */ - static final class Join { - - private final Table joinTable; - private final Column joinColumn; - private final Column parentId; - - Join(Table joinTable, Column joinColumn, Column parentId) { - - Assert.notNull(joinTable, "JoinTable must not be null"); - Assert.notNull(joinColumn, "JoinColumn must not be null"); - Assert.notNull(parentId, "ParentId must not be null"); - - this.joinTable = joinTable; - this.joinColumn = joinColumn; - this.parentId = parentId; - } - - Table getJoinTable() { - return this.joinTable; - } - - Column getJoinColumn() { - return this.joinColumn; - } - - Column getParentId() { - return this.parentId; - } - - @Override - public boolean equals(Object o) { + SelectBuilder.SelectWhere selectBuilder = selectBuilder(); + + Select select = applyQueryOnSelect(query, parameterSource, selectBuilder) // + .build(); + + return render(select); + } + + /** + * Constructs a single sql query that performs select based on the provided query and pagination information. + * Additional the bindings for the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null. + * @param pageable the pageable to perform on the select. + * @param parameterSource the source for holding the bindings. + * @return a non null query string. + */ + public String selectByQuery(Query query, MapSqlParameterSource parameterSource, Pageable pageable) { + + Assert.notNull(parameterSource, "parameterSource must not be null"); + + SelectBuilder.SelectWhere selectBuilder = selectBuilder(); + + // first apply query and then pagination. This means possible query sorting and limiting might be overwritten by the + // pagination. This is desired. + SelectBuilder.SelectOrdered selectOrdered = applyQueryOnSelect(query, parameterSource, selectBuilder); + selectOrdered = applyPagination(pageable, selectOrdered); + selectOrdered = selectOrdered.orderBy(extractOrderByFields(pageable.getSort())); + + Select select = selectOrdered.build(); + return render(select); + } + + /** + * Constructs a single sql query that performs select count based on the provided query for checking existence. + * Additional the bindings for the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null + * @param parameterSource the source for holding the bindings + * @return a non null query string. + */ + public String existsByQuery(Query query, MapSqlParameterSource parameterSource) { + + SelectBuilder.SelectJoin baseSelect = getExistsSelect(); + + Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // + .build(); + + return render(select); + } + + /** + * Constructs a single sql query that performs select count based on the provided query. Additional the bindings for + * the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null + * @param parameterSource the source for holding the bindings + * @return a non null query string. + */ + public String countByQuery(Query query, MapSqlParameterSource parameterSource) { + + Expression countExpression = Expressions.just("1"); + SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(countExpression); + + Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // + .build(); + + return render(select); + } + + /** + * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a + * COUNT(...) where the countExpressions are the parameters of the count. + * + * @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the + * columns and has only a count in the projection of the select. + */ + private SelectBuilder.SelectJoin getExistsSelect() { + + Table table = getTable(); + + SelectBuilder.SelectJoin baseSelect = StatementBuilder // + .select(dialect.getExistsFunction()) // + .from(table); + + // add possible joins + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), p -> true)) { + + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); + + // add a join if necessary + Join join = getJoin(extPath); + if (join != null) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + } + } + return baseSelect; + } + + /** + * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a + * COUNT(...) where the countExpressions are the parameters of the count. + * + * @param countExpressions the expression to use as count parameter. + * @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the + * columns and has only a count in the projection of the select. + */ + private SelectBuilder.SelectJoin getSelectCountWithExpression(Expression... countExpressions) { + + Assert.notNull(countExpressions, "countExpressions must not be null"); + Assert.state(countExpressions.length >= 1, "countExpressions must contain at least one expression"); + + Table table = getTable(); + + SelectBuilder.SelectJoin baseSelect = StatementBuilder // + .select(Functions.count(countExpressions)) // + .from(table); + + // add possible joins + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), p -> true)) { + + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); + + // add a join if necessary + Join join = getJoin(extPath); + if (join != null) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + } + } + return baseSelect; + } + + private SelectBuilder.SelectOrdered applyQueryOnSelect(Query query, MapSqlParameterSource parameterSource, + SelectBuilder.SelectWhere selectBuilder) { + + Table table = Table.create(this.entity.getQualifiedTableName()); + + SelectBuilder.SelectOrdered selectOrdered = query // + .getCriteria() // + .map(item -> this.applyCriteria(item, selectBuilder, parameterSource, table)) // + .orElse(selectBuilder); + + if (query.isSorted()) { + List sort = this.queryMapper.getMappedSort(table, query.getSort(), entity); + selectOrdered = selectBuilder.orderBy(sort); + } + + SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) selectOrdered; + + if (query.getLimit() > 0) { + limitable = limitable.limit(query.getLimit()); + } + + if (query.getOffset() > 0) { + limitable = limitable.offset(query.getOffset()); + } + return (SelectBuilder.SelectOrdered) limitable; + } + + SelectBuilder.SelectOrdered applyCriteria(@Nullable CriteriaDefinition criteria, + SelectBuilder.SelectWhere whereBuilder, MapSqlParameterSource parameterSource, Table table) { + + return criteria == null || criteria.isEmpty() // Check for null and empty criteria + ? whereBuilder // + : whereBuilder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity)); + } + + /** + * Value object representing a {@code JOIN} association. + */ + static final class Join { + + private final Table joinTable; + private final Column joinColumn; + private final Column parentId; + + Join(Table joinTable, Column joinColumn, Column parentId) { + + Assert.notNull(joinTable, "JoinTable must not be null"); + Assert.notNull(joinColumn, "JoinColumn must not be null"); + Assert.notNull(parentId, "ParentId must not be null"); + + this.joinTable = joinTable; + this.joinColumn = joinColumn; + this.parentId = parentId; + } + + Table getJoinTable() { + return this.joinTable; + } + + Column getJoinColumn() { + return this.joinColumn; + } + + Column getParentId() { + return this.parentId; + } + + @Override + public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Join join = (Join) o; - return joinTable.equals(join.joinTable) && joinColumn.equals(join.joinColumn) && parentId.equals(join.parentId); - } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Join join = (Join) o; + return joinTable.equals(join.joinTable) && joinColumn.equals(join.joinColumn) && parentId.equals(join.parentId); + } - @Override - public int hashCode() { - return Objects.hash(joinTable, joinColumn, parentId); - } + @Override + public int hashCode() { + return Objects.hash(joinTable, joinColumn, parentId); + } - @Override - public String toString() { - - return "Join{" + // - "joinTable=" + joinTable + // - ", joinColumn=" + joinColumn + // - ", parentId=" + parentId + // - '}'; - } - } - - /** - * Value object encapsulating column name caches. - * - * @author Mark Paluch - * @author Jens Schauder - */ - static class Columns { - - private final MappingContext, RelationalPersistentProperty> mappingContext; - private final JdbcConverter converter; - - private final List columnNames = new ArrayList<>(); - private final List idColumnNames = new ArrayList<>(); - private final List nonIdColumnNames = new ArrayList<>(); - private final Set readOnlyColumnNames = new HashSet<>(); - private final Set insertableColumns; - private final Set updatableColumns; - - Columns(RelationalPersistentEntity entity, - MappingContext, RelationalPersistentProperty> mappingContext, - JdbcConverter converter) { - - this.mappingContext = mappingContext; - this.converter = converter; - - populateColumnNameCache(entity, ""); - - Set insertable = new LinkedHashSet<>(nonIdColumnNames); - insertable.removeAll(readOnlyColumnNames); - - this.insertableColumns = Collections.unmodifiableSet(insertable); - - Set updatable = new LinkedHashSet<>(columnNames); - - updatable.removeAll(idColumnNames); - updatable.removeAll(readOnlyColumnNames); - - this.updatableColumns = Collections.unmodifiableSet(updatable); - } - - private void populateColumnNameCache(RelationalPersistentEntity entity, String prefix) { - - entity.doWithAll(property -> { + @Override + public String toString() { + + return "Join{" + // + "joinTable=" + joinTable + // + ", joinColumn=" + joinColumn + // + ", parentId=" + parentId + // + '}'; + } + } + + /** + * Value object encapsulating column name caches. + * + * @author Mark Paluch + * @author Jens Schauder + */ + static class Columns { + + private final MappingContext, RelationalPersistentProperty> mappingContext; + private final JdbcConverter converter; + + private final List columnNames = new ArrayList<>(); + private final List idColumnNames = new ArrayList<>(); + private final List nonIdColumnNames = new ArrayList<>(); + private final Set readOnlyColumnNames = new HashSet<>(); + private final Set insertableColumns; + private final Set updatableColumns; + + Columns(RelationalPersistentEntity entity, + MappingContext, RelationalPersistentProperty> mappingContext, + JdbcConverter converter) { + + this.mappingContext = mappingContext; + this.converter = converter; + + populateColumnNameCache(entity, ""); + + Set insertable = new LinkedHashSet<>(nonIdColumnNames); + insertable.removeAll(readOnlyColumnNames); + + this.insertableColumns = Collections.unmodifiableSet(insertable); + + Set updatable = new LinkedHashSet<>(columnNames); + + updatable.removeAll(idColumnNames); + updatable.removeAll(readOnlyColumnNames); + + this.updatableColumns = Collections.unmodifiableSet(updatable); + } + + private void populateColumnNameCache(RelationalPersistentEntity entity, String prefix) { + + entity.doWithAll(property -> { - // the referencing column of referenced entity is expected to be on the other side of the relation - if (!property.isEntity()) { - initSimpleColumnName(property, prefix); - } else if (property.isEmbedded()) { - initEmbeddedColumnNames(property, prefix); - } - }); - } - - private void initSimpleColumnName(RelationalPersistentProperty property, String prefix) { - - SqlIdentifier columnName = property.getColumnName().transform(prefix::concat); - - columnNames.add(columnName); - - if (!property.getOwner().isIdProperty(property)) { - nonIdColumnNames.add(columnName); - } else { - idColumnNames.add(columnName); - } - - if (!property.isWritable()) { - readOnlyColumnNames.add(columnName); - } - } - - private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { - - String embeddedPrefix = property.getEmbeddedPrefix(); - - RelationalPersistentEntity embeddedEntity = mappingContext - .getRequiredPersistentEntity(converter.getColumnType(property)); - - populateColumnNameCache(embeddedEntity, prefix + embeddedPrefix); - } + // the referencing column of referenced entity is expected to be on the other side of the relation + if (!property.isEntity()) { + initSimpleColumnName(property, prefix); + } else if (property.isEmbedded()) { + initEmbeddedColumnNames(property, prefix); + } + }); + } + + private void initSimpleColumnName(RelationalPersistentProperty property, String prefix) { + + SqlIdentifier columnName = property.getColumnName().transform(prefix::concat); + + columnNames.add(columnName); + + if (!property.getOwner().isIdProperty(property)) { + nonIdColumnNames.add(columnName); + } else { + idColumnNames.add(columnName); + } + + if (!property.isWritable()) { + readOnlyColumnNames.add(columnName); + } + } + + private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { + + String embeddedPrefix = property.getEmbeddedPrefix(); + + RelationalPersistentEntity embeddedEntity = mappingContext + .getRequiredPersistentEntity(converter.getColumnType(property)); + + populateColumnNameCache(embeddedEntity, prefix + embeddedPrefix); + } - /** - * @return Column names that can be used for {@code INSERT}. - */ - Set getInsertableColumns() { - return insertableColumns; - } + /** + * @return Column names that can be used for {@code INSERT}. + */ + Set getInsertableColumns() { + return insertableColumns; + } - /** - * @return Column names that can be used for {@code UPDATE}. - */ - Set getUpdatableColumns() { - return updatableColumns; - } - } + /** + * @return Column names that can be used for {@code UPDATE}. + */ + Set getUpdatableColumns() { + return updatableColumns; + } + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java index e500ab7569..f9fa94a4eb 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java @@ -38,7 +38,7 @@ class SqlContext { SqlContext(RelationalPersistentEntity entity) { this.entity = entity; - this.table = Table.create(entity.getTableName()); + this.table = Table.create(entity.getQualifiedTableName()); } Column getIdColumn() { @@ -56,7 +56,7 @@ Table getTable() { Table getTable(PersistentPropertyPathExtension path) { SqlIdentifier tableAlias = path.getTableAlias(); - Table table = Table.create(path.getTableName()); + Table table = Table.create(path.getQualifiedTableName()); return tableAlias == null ? table : table.as(tableAlias); } 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 d343e4fc70..3ace05fcfc 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 @@ -15,7 +15,6 @@ */ package org.springframework.data.jdbc.core; -import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.SoftAssertions.*; import static org.springframework.data.relational.core.sql.SqlIdentifier.*; @@ -102,13 +101,13 @@ void getTableName() { assertSoftly(softly -> { - softly.assertThat(extPath(entity).getTableName()).isEqualTo(quoted("DUMMY_ENTITY")); - softly.assertThat(extPath("second").getTableName()).isEqualTo(quoted("SECOND")); - softly.assertThat(extPath("second.third2").getTableName()).isEqualTo(quoted("SECOND")); - softly.assertThat(extPath("second.third2.value").getTableName()).isEqualTo(quoted("SECOND")); - softly.assertThat(extPath("secondList.third2").getTableName()).isEqualTo(quoted("SECOND")); - softly.assertThat(extPath("secondList.third2.value").getTableName()).isEqualTo(quoted("SECOND")); - softly.assertThat(extPath("secondList").getTableName()).isEqualTo(quoted("SECOND")); + softly.assertThat(extPath(entity).getQualifiedTableName()).isEqualTo(quoted("DUMMY_ENTITY")); + softly.assertThat(extPath("second").getQualifiedTableName()).isEqualTo(quoted("SECOND")); + softly.assertThat(extPath("second.third2").getQualifiedTableName()).isEqualTo(quoted("SECOND")); + softly.assertThat(extPath("second.third2.value").getQualifiedTableName()).isEqualTo(quoted("SECOND")); + softly.assertThat(extPath("secondList.third2").getQualifiedTableName()).isEqualTo(quoted("SECOND")); + softly.assertThat(extPath("secondList.third2.value").getQualifiedTableName()).isEqualTo(quoted("SECOND")); + softly.assertThat(extPath("secondList").getQualifiedTableName()).isEqualTo(quoted("SECOND")); }); } @@ -228,10 +227,14 @@ void equalsWorks() { void isWritable() { assertSoftly(softly -> { - softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("withId"))).describedAs("simple path is writable").isTrue(); - softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("secondList.third2"))).describedAs("long path is writable").isTrue(); - softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("second"))).describedAs("simple read only path is not writable").isFalse(); - softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("second.third"))).describedAs("long path containing read only element is not writable").isFalse(); + softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("withId"))) + .describedAs("simple path is writable").isTrue(); + softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("secondList.third2"))) + .describedAs("long path is writable").isTrue(); + softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("second"))) + .describedAs("simple read only path is not writable").isFalse(); + softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("second.third"))) + .describedAs("long path containing read only element is not writable").isFalse(); }); } @@ -250,8 +253,7 @@ PersistentPropertyPath createSimplePath(String pat @SuppressWarnings("unused") static class DummyEntity { @Id Long entityId; - @ReadOnlyProperty - Second second; + @ReadOnlyProperty Second second; @Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "sec") Second second2; @Embedded(onEmpty = OnEmpty.USE_NULL) Second second3; List secondList; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java index c8ccff0835..85dc5b8e72 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -23,8 +23,8 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; -import org.springframework.data.relational.core.dialect.AnsiDialect; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.dialect.AnsiDialect; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -37,9 +37,9 @@ * @author Greg Turnquist * @author Mark Paluch */ -public class SqlGeneratorFixedNamingStrategyUnitTests { +class SqlGeneratorFixedNamingStrategyUnitTests { - final NamingStrategy fixedCustomTablePrefixStrategy = new NamingStrategy() { + private final NamingStrategy fixedCustomTablePrefixStrategy = new NamingStrategy() { @Override public String getSchema() { @@ -57,7 +57,7 @@ public String getColumnName(RelationalPersistentProperty property) { } }; - final NamingStrategy upperCaseLowerCaseStrategy = new NamingStrategy() { + private final NamingStrategy upperCaseLowerCaseStrategy = new NamingStrategy() { @Override public String getTableName(Class type) { @@ -73,7 +73,7 @@ public String getColumnName(RelationalPersistentProperty property) { private RelationalMappingContext context = new JdbcMappingContext(); @Test // DATAJDBC-107 - public void findOneWithOverriddenFixedTableName() { + void findOneWithOverriddenFixedTableName() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); @@ -96,7 +96,7 @@ public void findOneWithOverriddenFixedTableName() { } @Test // DATAJDBC-107 - public void findOneWithUppercasedTablesAndLowercasedColumns() { + void findOneWithUppercasedTablesAndLowercasedColumns() { SqlGenerator sqlGenerator = configureSqlGenerator(upperCaseLowerCaseStrategy); @@ -115,7 +115,7 @@ public void findOneWithUppercasedTablesAndLowercasedColumns() { } @Test // DATAJDBC-107 - public void cascadingDeleteFirstLevel() { + void cascadingDeleteFirstLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); @@ -126,7 +126,7 @@ public void cascadingDeleteFirstLevel() { } @Test // DATAJDBC-107 - public void cascadingDeleteAllSecondLevel() { + void cascadingDeleteAllSecondLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); @@ -134,14 +134,14 @@ public void cascadingDeleteAllSecondLevel() { assertThat(sql) .isEqualTo("DELETE FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\".\"REFERENCED_ENTITY\" IN " + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" IN " + "(SELECT \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_L1ID\" " + "FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" " + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"DUMMY_ENTITY\" = :rootId)"); } @Test // DATAJDBC-107 - public void deleteAll() { + void deleteAll() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); @@ -151,7 +151,7 @@ public void deleteAll() { } @Test // DATAJDBC-107 - public void cascadingDeleteAllFirstLevel() { + void cascadingDeleteAllFirstLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); @@ -162,7 +162,7 @@ public void cascadingDeleteAllFirstLevel() { } @Test // DATAJDBC-107 - public void cascadingDeleteSecondLevel() { + void cascadingDeleteSecondLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); @@ -170,14 +170,14 @@ public void cascadingDeleteSecondLevel() { assertThat(sql) .isEqualTo("DELETE FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\".\"REFERENCED_ENTITY\" IN " + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" IN " + "(SELECT \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_L1ID\" " + "FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" " + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"DUMMY_ENTITY\" IS NOT NULL)"); } @Test // DATAJDBC-113 - public void deleteByList() { + void deleteByList() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index 5b169a9f0f..e613b5e46a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -18,6 +18,7 @@ import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.SoftAssertions.*; +import static org.springframework.data.relational.core.mapping.ForeignKeyNaming.*; import static org.springframework.data.relational.core.sql.SqlIdentifier.*; import java.util.Map; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Version; @@ -41,7 +43,7 @@ import org.springframework.data.relational.core.dialect.PostgresDialect; import org.springframework.data.relational.core.dialect.SqlServerDialect; import org.springframework.data.relational.core.mapping.Column; -import org.springframework.data.relational.core.mapping.NamingStrategy; +import org.springframework.data.relational.core.mapping.DefaultNamingStrategy; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -76,8 +78,8 @@ class SqlGeneratorUnitTests { private static final Identifier BACKREF = Identifier.of(unquoted("backref"), "some-value", String.class); - private final NamingStrategy namingStrategy = new PrefixingNamingStrategy(); - private final RelationalMappingContext context = new JdbcMappingContext(namingStrategy); + private final PrefixingNamingStrategy namingStrategy = new PrefixingNamingStrategy(); + private RelationalMappingContext context = new JdbcMappingContext(namingStrategy); private final JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); @@ -747,7 +749,6 @@ void columnForIndirectProperty() { @Test // DATAJDBC-340 void noColumnForReferencedEntity() { - assertThat(generatedColumn("ref", DummyEntity.class)).isNull(); } @@ -763,7 +764,7 @@ void columnForReferencedEntityWithoutId() { @Test // GH-1192 void selectByQueryValidTest() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); DummyEntity probe = new DummyEntity(); probe.name = "Diego"; @@ -776,8 +777,8 @@ void selectByQueryValidTest() { String generatedSQL = sqlGenerator.selectByQuery(query, parameterSource); assertThat(generatedSQL).isNotNull().contains(":x_name"); - assertThat(parameterSource.getValues()) // - .containsOnly(entry("x_name", probe.name)); + assertThat(parameterSource.getValues()).hasSize(1) // + .containsEntry("x_name", probe.name); } @Test // GH-1329 @@ -808,8 +809,8 @@ void existsByQuerySimpleValidTest() { String generatedSQL = sqlGenerator.existsByQuery(query, parameterSource); assertThat(generatedSQL).isNotNull().contains(":x_name"); - assertThat(parameterSource.getValues()) // - .containsOnly(entry("x_name", probe.name)); + assertThat(parameterSource.getValues()).hasSize(1) // + .containsEntry("x_name", probe.name); } @Test // GH-1192 @@ -831,8 +832,8 @@ void countByQuerySimpleValidTest() { .containsIgnoringCase("COUNT(1)") // .contains(":x_name"); - assertThat(parameterSource.getValues()) // - .containsOnly(entry("x_name", probe.name)); + assertThat(parameterSource.getValues()).hasSize(1) // + .containsEntry("x_name", probe.name); } @Test // GH-1192 @@ -858,8 +859,57 @@ void selectByQueryPaginationValidTest() { .containsIgnoringCase("LIMIT 1") // .containsIgnoringCase("OFFSET 2 LIMIT 1"); - assertThat(parameterSource.getValues()) // - .containsOnly(entry("x_name", probe.name)); + assertThat(parameterSource.getValues()).hasSize(1) // + .containsEntry("x_name", probe.name); + } + + @Test // GH-1161 + void backReferenceShouldConsiderRenamedParent() { + + namingStrategy.setForeignKeyNaming(APPLY_RENAMING); + context = new JdbcMappingContext(namingStrategy); + + String sql = sqlGenerator.createDeleteInByPath(getPath("ref", RenamedDummy.class)); + + assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.renamed IN (:ids)"); + } + + @Test // GH-1161 + void backReferenceShouldIgnoreRenamedParent() { + + namingStrategy.setForeignKeyNaming(IGNORE_RENAMING); + context = new JdbcMappingContext(namingStrategy); + + String sql = sqlGenerator.createDeleteInByPath(getPath("ref", RenamedDummy.class)); + + assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.renamed_dummy IN (:ids)"); + } + + @Test // GH-1161 + void keyColumnShouldConsiderRenamedParent() { + + namingStrategy.setForeignKeyNaming(APPLY_RENAMING); + context = new JdbcMappingContext(namingStrategy); + + SqlGenerator sqlGenerator = createSqlGenerator(ReferencedEntity.class); + String sql = sqlGenerator.getFindAllByProperty(Identifier.of(unquoted("parentId"), 23, RenamedDummy.class), + getPath("ref", RenamedDummy.class)); + + assertThat(sql).contains("referenced_entity.renamed_key AS renamed_key", "WHERE referenced_entity.parentId"); + } + + @Test // GH-1161 + void keyColumnShouldIgnoreRenamedParent() { + + namingStrategy.setForeignKeyNaming(IGNORE_RENAMING); + context = new JdbcMappingContext(namingStrategy); + + SqlGenerator sqlGenerator = createSqlGenerator(ReferencedEntity.class); + String sql = sqlGenerator.getFindAllByProperty(Identifier.of(unquoted("parentId"), 23, RenamedDummy.class), + getPath("ref", RenamedDummy.class)); + + assertThat(sql).contains("referenced_entity.renamed_dummy_key AS renamed_dummy_key", + "WHERE referenced_entity.parentId"); } @Nullable @@ -895,6 +945,15 @@ static class DummyEntity { Map mappedReference; } + @SuppressWarnings("unused") + @org.springframework.data.relational.core.mapping.Table("renamed") + static class RenamedDummy { + + @Id Long id; + String name; + Map ref; + } + @SuppressWarnings("unused") static class VersionedEntity extends DummyEntity { @Version Integer version; @@ -936,11 +995,11 @@ static class OtherAggregate { String name; } - private static class PrefixingNamingStrategy implements NamingStrategy { + private static class PrefixingNamingStrategy extends DefaultNamingStrategy { @Override public String getColumnName(RelationalPersistentProperty property) { - return "x_" + NamingStrategy.super.getColumnName(property); + return "x_" + super.getColumnName(property); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index e888226a34..975b4f075c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -67,7 +67,7 @@ public class PartTreeJdbcQueryUnitTests { private static final String TABLE = "\"users\""; private static final String ALL_FIELDS = "\"users\".\"ID\" AS \"ID\", \"users\".\"AGE\" AS \"AGE\", \"users\".\"ACTIVE\" AS \"ACTIVE\", \"users\".\"LAST_NAME\" AS \"LAST_NAME\", \"users\".\"FIRST_NAME\" AS \"FIRST_NAME\", \"users\".\"DATE_OF_BIRTH\" AS \"DATE_OF_BIRTH\", \"users\".\"HOBBY_REFERENCE\" AS \"HOBBY_REFERENCE\", \"hated\".\"NAME\" AS \"HATED_NAME\", \"users\".\"USER_CITY\" AS \"USER_CITY\", \"users\".\"USER_STREET\" AS \"USER_STREET\""; - private static final String JOIN_CLAUSE = "FROM \"users\" LEFT OUTER JOIN \"HOBBY\" \"hated\" ON \"hated\".\"USER\" = \"users\".\"ID\""; + private static final String JOIN_CLAUSE = "FROM \"users\" LEFT OUTER JOIN \"HOBBY\" \"hated\" ON \"hated\".\"USERS\" = \"users\".\"ID\""; private static final String BASE_SELECT = "SELECT " + ALL_FIELDS + " " + JOIN_CLAUSE; JdbcMappingContext mappingContext = new JdbcMappingContext(); diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 14d7909d7b..dd571f02d3 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.0.0-SNAPSHOT + 3.0.0-1147-fk-naming-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-1147-fk-naming-SNAPSHOT diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java index dc36f405ea..5913fc4c55 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -305,7 +305,7 @@ public PreparedOperation processNamedParameters(String query, NamedParameterP */ @Override public SqlIdentifier getTableName(Class type) { - return getRequiredPersistentEntity(type).getTableName(); + return getRequiredPersistentEntity(type).getQualifiedTableName(); } /* diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java index 7379f4eda8..aca1f1a4c2 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java @@ -543,7 +543,7 @@ public Mono insert(T entity) throws DataAccessException { Assert.notNull(entity, "Entity must not be null"); - return doInsert(entity, getRequiredEntity(entity).getTableName()); + return doInsert(entity, getRequiredEntity(entity).getQualifiedTableName()); } Mono doInsert(T entity, SqlIdentifier tableName) { @@ -646,7 +646,7 @@ public Mono update(T entity) throws DataAccessException { Assert.notNull(entity, "Entity must not be null"); - return doUpdate(entity, getRequiredEntity(entity).getTableName()); + return doUpdate(entity, getRequiredEntity(entity).getQualifiedTableName()); } private Mono doUpdate(T entity, SqlIdentifier tableName) { @@ -719,13 +719,13 @@ private Mono doUpdate(T entity, SqlIdentifier tableName, RelationalPersis private String formatOptimisticLockingExceptionMessage(T entity, RelationalPersistentEntity persistentEntity) { return String.format("Failed to update table [%s]; Version does not match for row with Id [%s]", - persistentEntity.getTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier()); + persistentEntity.getQualifiedTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier()); } private String formatTransientEntityExceptionMessage(T entity, RelationalPersistentEntity persistentEntity) { return String.format("Failed to update table [%s]; Row with Id [%s] does not exist", - persistentEntity.getTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier()); + persistentEntity.getQualifiedTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier()); } @SuppressWarnings("unchecked") @@ -823,14 +823,14 @@ private Query getByIdQuery(T entity, RelationalPersistentEntity persisten } SqlIdentifier getTableName(Class entityClass) { - return getRequiredEntity(entityClass).getTableName(); + return getRequiredEntity(entityClass).getQualifiedTableName(); } SqlIdentifier getTableNameOrEmpty(Class entityClass) { RelationalPersistentEntity entity = this.mappingContext.getPersistentEntity(entityClass); - return entity != null ? entity.getTableName() : SqlIdentifier.EMPTY; + return entity != null ? entity.getQualifiedTableName() : SqlIdentifier.EMPTY; } private RelationalPersistentEntity getRequiredEntity(Class entityClass) { diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index e9d0455c68..1e54949f91 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.0.0-SNAPSHOT + 3.0.0-1147-fk-naming-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-1147-fk-naming-SNAPSHOT diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/CachingNamingStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/CachingNamingStrategy.java index 3a9837ea3c..e93194224d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/CachingNamingStrategy.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/CachingNamingStrategy.java @@ -35,7 +35,6 @@ class CachingNamingStrategy implements NamingStrategy { private final Map columnNames = new ConcurrentHashMap<>(); private final Map keyColumns = new ConcurrentHashMap<>(); - private final Map, String> qualifiedTableNames = new ConcurrentReferenceHashMap<>(); private final Map, String> tableNames = new ConcurrentReferenceHashMap<>(); private final Lazy schema; @@ -82,4 +81,5 @@ public String getSchema() { public String getColumnName(RelationalPersistentProperty property) { return columnNames.computeIfAbsent(property, delegate::getColumnName); } + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategy.java new file mode 100644 index 0000000000..e22cde1d86 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategy.java @@ -0,0 +1,59 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping; + +import org.springframework.util.Assert; + +/** + * The default naming strategy used by Spring Data Relational. Names are in {@code SNAKE_CASE}. + * + * @author Jens Schauder + * @since 3.0 + */ +public class DefaultNamingStrategy implements NamingStrategy { + + private ForeignKeyNaming foreignKeyNaming = ForeignKeyNaming.APPLY_RENAMING; + + public void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) { + + Assert.notNull(foreignKeyNaming, "foreignKeyNaming must not be null"); + + this.foreignKeyNaming = foreignKeyNaming; + } + + @Override + public String getReverseColumnName(RelationalPersistentProperty property) { + + return getColumnNameReferencing(property.getOwner()); + } + + @Override + public String getReverseColumnName(PersistentPropertyPathExtension path) { + + RelationalPersistentEntity leafEntity = path.getIdDefiningParentPath().getRequiredLeafEntity(); + + return getColumnNameReferencing(leafEntity); + } + + private String getColumnNameReferencing(RelationalPersistentEntity leafEntity) { + + if (foreignKeyNaming == ForeignKeyNaming.IGNORE_RENAMING) { + return getTableName(leafEntity.getType()); + } + + return leafEntity.getTableName().getReference(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ForeignKeyNaming.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ForeignKeyNaming.java new file mode 100644 index 0000000000..e5f98123f1 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ForeignKeyNaming.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping; + +/** + * Enum for determining how the names of back references should get generated. + * + * @author Jens Schauder + * @since 2.4 + */ +public enum ForeignKeyNaming { + /** + * This strategy takes names specified via {@link Table} annotation into account. + */ + APPLY_RENAMING, + /** + * This strategy does not take names specified via {@link Table} annotation into account. + */ + IGNORE_RENAMING +} 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 bc1fdaeabc..05b6b227fc 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 @@ -38,8 +38,15 @@ public interface NamingStrategy { * Empty implementation of the interface utilizing only the default implementation. *

* Using this avoids creating essentially the same class over and over again. + * + * @deprecated use {@link DefaultNamingStrategy#INSTANCE} instead. */ - NamingStrategy INSTANCE = new NamingStrategy() {}; + @Deprecated(since = "2.4") NamingStrategy INSTANCE = new DefaultNamingStrategy() { + @Override + public void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) { + throw new UnsupportedOperationException("Cannot update immutable DefaultNamingStrategy"); + } + }; /** * Defaults to no schema. @@ -75,7 +82,7 @@ default String getColumnName(RelationalPersistentProperty property) { /** * For a reference A -> B this is the name in the table for B which references A. * - * @param property The property who's column name in the owner table is required + * @param property The property whose column name in the owner table is required * @return a column name. Must not be {@code null}. */ default String getReverseColumnName(RelationalPersistentProperty property) { @@ -86,8 +93,7 @@ default String getReverseColumnName(RelationalPersistentProperty property) { } default String getReverseColumnName(PersistentPropertyPathExtension path) { - - return getTableName(path.getIdDefiningParentPath().getLeafEntity().getType()); + return getTableName(path.getIdDefiningParentPath().getRequiredLeafEntity().getType()); } /** diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java index 068f198ddb..865d527f15 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java @@ -138,6 +138,30 @@ public RelationalPersistentEntity getLeafEntity() { return path == null ? entity : context.getPersistentEntity(path.getRequiredLeafProperty().getActualType()); } + /** + * The {@link RelationalPersistentEntity} associated with the leaf of this path or throw {@link IllegalStateException} + * if the leaf cannot be resolved. + * + * @return the required {@link RelationalPersistentEntity} associated with the leaf of this path. + * @since 3.0 + * @throws IllegalStateException if the persistent entity cannot be resolved. + */ + public RelationalPersistentEntity getRequiredLeafEntity() { + + RelationalPersistentEntity entity = getLeafEntity(); + + if (entity == null) { + + if (this.path == null) { + throw new IllegalStateException("Couldn't resolve leaf PersistentEntity absent path"); + } + throw new IllegalStateException(String.format("Couldn't resolve leaf PersistentEntity for type %s", + path.getRequiredLeafProperty().getActualType())); + } + + return entity; + } + /** * @return {@literal true} when this is an empty path or the path references an entity. */ @@ -230,10 +254,21 @@ public PersistentPropertyPathExtension getIdDefiningParentPath() { return parent; } + /** + * The fully qualified name of the table this path is tied to or of the longest ancestor path that is actually tied to + * a table. + * + * @return the name of the table. Guaranteed to be not {@literal null}. + */ + public SqlIdentifier getQualifiedTableName() { + return getTableOwningAncestor().getRequiredLeafEntity().getQualifiedTableName(); + } + /** * The name of the table this path is tied to or of the longest ancestor path that is actually tied to a table. * * @return the name of the table. Guaranteed to be not {@literal null}. + * @since 3.0 */ public SqlIdentifier getTableName() { return getTableOwningAncestor().getRequiredLeafEntity().getTableName(); @@ -442,10 +477,6 @@ private SqlIdentifier assembleColumnName(SqlIdentifier suffix) { return getParentPath().assembleColumnName(suffix.transform(embeddedPrefix::concat)); } - private RelationalPersistentEntity getRequiredLeafEntity() { - return path == null ? entity : context.getRequiredPersistentEntity(path.getRequiredLeafProperty().getActualType()); - } - private SqlIdentifier prefixWithTableAlias(SqlIdentifier columnName) { SqlIdentifier tableAlias = getTableAlias(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index 11a43248de..f46b43841f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -41,7 +41,7 @@ public class RelationalMappingContext * Creates a new {@link RelationalMappingContext}. */ public RelationalMappingContext() { - this(NamingStrategy.INSTANCE); + this(new DefaultNamingStrategy()); } /** @@ -101,4 +101,5 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert public NamingStrategy getNamingStrategy() { return this.namingStrategy; } + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java index 15b3234763..8d3f8f96ea 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java @@ -24,20 +24,32 @@ * * @author Jens Schauder * @author Oliver Gierke + * @author Mark Paluch */ public interface RelationalPersistentEntity extends MutablePersistentEntity { /** - * Returns the name of the table backing the given entity. + * Returns the unqualified name of the table (i.e. without schema or owner) backing the given entity. * * @return the table name. */ SqlIdentifier getTableName(); + /** + * Returns the qualified name of the table backing the given entity, including the schema. + * + * @return the table name including the schema if there is any specified. + * @since 3.0 + */ + default SqlIdentifier getQualifiedTableName() { + return getTableName(); + } + /** * Returns the column representing the identifier. * * @return will never be {@literal null}. */ SqlIdentifier getIdColumn(); + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java index 9021f6a9e3..0ffccabed8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java @@ -51,16 +51,11 @@ class RelationalPersistentEntityImpl extends BasicPersistentEntity Optional.ofNullable(findAnnotation(Table.class)) - .map(Table::value) - .filter(StringUtils::hasText) - .map(this::createSqlIdentifier) - ); - - this.schemaName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Table.class)) - .map(Table::schema) - .filter(StringUtils::hasText) - .map(this::createSqlIdentifier)); + this.tableName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Table.class)).map(Table::value) + .filter(StringUtils::hasText).map(this::createSqlIdentifier)); + + this.schemaName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Table.class)).map(Table::schema) + .filter(StringUtils::hasText).map(this::createSqlIdentifier)); } private SqlIdentifier createSqlIdentifier(String name) { @@ -82,32 +77,39 @@ public void setForceQuote(boolean forceQuote) { @Override public SqlIdentifier getTableName() { + Optional explicitlySpecifiedTableName = tableName.get(); + SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); + + return explicitlySpecifiedTableName.orElse(schemalessTableIdentifier); + } + + @Override + public SqlIdentifier getQualifiedTableName() { + SqlIdentifier schema = determineCurrentEntitySchema(); Optional explicitlySpecifiedTableName = tableName.get(); - final SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); + SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); if (schema == null) { return explicitlySpecifiedTableName.orElse(schemalessTableIdentifier); } - return explicitlySpecifiedTableName - .map(sqlIdentifier -> SqlIdentifier.from(schema, sqlIdentifier)) + return explicitlySpecifiedTableName.map(sqlIdentifier -> SqlIdentifier.from(schema, sqlIdentifier)) .orElse(SqlIdentifier.from(schema, schemalessTableIdentifier)); } /** * @return {@link SqlIdentifier} representing the current entity schema. If the schema is not specified, neither - * explicitly, nor via {@link NamingStrategy}, then return {@link null} + * explicitly, nor via {@link NamingStrategy}, then return {@link null} */ @Nullable private SqlIdentifier determineCurrentEntitySchema() { Optional explicitlySpecifiedSchema = schemaName.get(); return explicitlySpecifiedSchema.orElseGet( - () -> StringUtils.hasText(namingStrategy.getSchema()) - ? createDerivedSqlIdentifier(namingStrategy.getSchema()) - : null); + () -> StringUtils.hasText(namingStrategy.getSchema()) ? createDerivedSqlIdentifier(namingStrategy.getSchema()) + : null); } @Override diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java index a633302a3d..89077b3a54 100755 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java @@ -50,7 +50,7 @@ public Class getJavaType() { } public SqlIdentifier getTableName() { - return tableEntity.getTableName(); + return tableEntity.getQualifiedTableName(); } public RelationalPersistentEntity getTableEntity() { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java index 0af57a0db7..e5ea18f9d5 100755 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java @@ -87,7 +87,7 @@ private MappingRelationalEntityInformation(RelationalPersistentEntity entity, } public SqlIdentifier getTableName() { - return customTableName == null ? entityMetadata.getTableName() : customTableName; + return customTableName == null ? entityMetadata.getQualifiedTableName() : customTableName; } public String getIdAttribute() { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java index 8008a3c96c..13400dff99 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java @@ -32,43 +32,55 @@ * @author Mark Paluch * @author Mikhail Polivakha */ -public class RelationalPersistentEntityImplUnitTests { +class RelationalPersistentEntityImplUnitTests { - RelationalMappingContext mappingContext = new RelationalMappingContext(); + private RelationalMappingContext mappingContext = new RelationalMappingContext(); @Test // DATAJDBC-106 - public void discoversAnnotatedTableName() { + void discoversAnnotatedTableName() { - RelationalPersistentEntity entity = mappingContext.getPersistentEntity(DummySubEntity.class); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(DummySubEntity.class); assertThat(entity.getTableName()).isEqualTo(quoted("dummy_sub_entity")); + assertThat(entity.getQualifiedTableName()).isEqualTo(quoted("dummy_sub_entity")); + assertThat(entity.getTableName()).isEqualTo(quoted("dummy_sub_entity")); } @Test // DATAJDBC-294 - public void considerIdColumnName() { + void considerIdColumnName() { - RelationalPersistentEntity entity = mappingContext.getPersistentEntity(DummySubEntity.class); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(DummySubEntity.class); assertThat(entity.getIdColumn()).isEqualTo(quoted("renamedId")); } @Test // DATAJDBC-296 - public void emptyTableAnnotationFallsBackToNamingStrategy() { + void emptyTableAnnotationFallsBackToNamingStrategy() { - RelationalPersistentEntity entity = mappingContext.getPersistentEntity(DummyEntityWithEmptyAnnotation.class); + RelationalPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(DummyEntityWithEmptyAnnotation.class); assertThat(entity.getTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); + assertThat(entity.getQualifiedTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); + assertThat(entity.getTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); } @Test // DATAJDBC-491 - public void namingStrategyWithSchemaReturnsCompositeTableName() { + void namingStrategyWithSchemaReturnsCompositeTableName() { mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); - RelationalPersistentEntity entity = mappingContext.getPersistentEntity(DummyEntityWithEmptyAnnotation.class); + RelationalPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(DummyEntityWithEmptyAnnotation.class); + + SqlIdentifier simpleExpected = quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION"); + SqlIdentifier fullExpected = SqlIdentifier.from(quoted("MY_SCHEMA"), simpleExpected); + assertThat(entity.getQualifiedTableName()) + .isEqualTo(fullExpected); assertThat(entity.getTableName()) - .isEqualTo(SqlIdentifier.from(quoted("MY_SCHEMA"), quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION"))); - assertThat(entity.getTableName().toSql(IdentifierProcessing.ANSI)) + .isEqualTo(simpleExpected); + + assertThat(entity.getQualifiedTableName().toSql(IdentifierProcessing.ANSI)) .isEqualTo("\"MY_SCHEMA\".\"DUMMY_ENTITY_WITH_EMPTY_ANNOTATION\""); } @@ -76,30 +88,32 @@ public void namingStrategyWithSchemaReturnsCompositeTableName() { void testRelationalPersistentEntitySchemaNameChoice() { mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); - RelationalPersistentEntity persistentEntity = mappingContext.getPersistentEntity(EntityWithSchemaAndName.class); - - SqlIdentifier tableName = persistentEntity.getTableName(); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndName.class); - assertThat(tableName).isEqualTo(SqlIdentifier.from(SqlIdentifier.quoted("DART_VADER"), quoted("I_AM_THE_SENATE"))); + SqlIdentifier simpleExpected = quoted("I_AM_THE_SENATE"); + SqlIdentifier expected = SqlIdentifier.from(quoted("DART_VADER"), simpleExpected); + assertThat(entity.getQualifiedTableName()).isEqualTo(expected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); } @Test // GH-1099 void specifiedSchemaGetsCombinedWithNameFromNamingStrategy() { - RelationalPersistentEntity persistentEntity = mappingContext.getPersistentEntity(EntityWithSchema.class); - - SqlIdentifier tableName = persistentEntity.getTableName(); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchema.class); - assertThat(tableName).isEqualTo(SqlIdentifier.from(quoted("ANAKYN_SKYWALKER"), quoted("ENTITY_WITH_SCHEMA"))); + SqlIdentifier simpleExpected = quoted("ENTITY_WITH_SCHEMA"); + SqlIdentifier expected = SqlIdentifier.from(quoted("ANAKYN_SKYWALKER"), simpleExpected); + assertThat(entity.getQualifiedTableName()).isEqualTo(expected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); } @Table(schema = "ANAKYN_SKYWALKER") - static class EntityWithSchema { + private static class EntityWithSchema { @Id private Long id; } @Table(schema = "DART_VADER", name = "I_AM_THE_SENATE") - static class EntityWithSchemaAndName { + private static class EntityWithSchemaAndName { @Id private Long id; } diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index ac78ad9e02..940a48e33b 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -216,22 +216,23 @@ The properties of the following types are currently supported: * References to other entities. They are considered a one-to-one relationship, or an embedded type. It is optional for one-to-one relationship entities to have an `id` attribute. -The table of the referenced entity is expected to have an additional column named the same as the table of the referencing entity. -You can change this name by implementing `NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)`. +The table of the referenced entity is expected to have an additional column with a name based on the referencing entity see <>. Embedded entities do not need an `id`. If one is present it gets ignored. * `Set` is considered a one-to-many relationship. -The table of the referenced entity is expected to have an additional column named the same as the table of the referencing entity. -You can change this name by implementing `NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)`. +The table of the referenced entity is expected to have an additional column with a name based on the referencing entity see <>. * `Map` is considered a qualified one-to-many relationship. -The table of the referenced entity is expected to have two additional columns: One named the same as the table of the referencing entity for the foreign key and one with the same name and an additional `_key` suffix for the map key. +The table of the referenced entity is expected to have two additional columns: One named based on the referencing entity for the foreign key (see <>) and one with the same name and an additional `_key` suffix for the map key. You can change this behavior by implementing `NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)` and `NamingStrategy.getKeyColumn(RelationalPersistentProperty property)`, respectively. Alternatively you may annotate the attribute with `@MappedCollection(idColumn="your_column_name", keyColumn="your_key_column_name")` * `List` is mapped as a `Map`. +[[jdbc.entity-persistence.types.referenced-entities]] +==== Referenced Entities + The handling of referenced entities is limited. This is based on the idea of aggregate roots as described above. If you reference another entity, that entity is, by definition, part of your aggregate. @@ -240,10 +241,23 @@ This also means references are 1-1 or 1-n, but not n-1 or n-m. If you have n-1 or n-m references, you are, by definition, dealing with two separate aggregates. References between those may be encoded as simple `id` values, which map properly with Spring Data JDBC. -A better way to encode these is to make them instances of `AggregateReference`. +A better way to encode these, is to make them instances of `AggregateReference`. An `AggregateReference` is a wrapper around an id value which marks that value as a reference to a different aggregate. Also, the type of that aggregate is encoded in a type parameter. +[[jdbc.entity-persistence.types.backrefs]] +==== Back References + +All references in an aggregate result in a foreign key relationship in the opposite direction in the database. +By default, the name of the foreign key column is the table name of the referencing entity. + +Alternatively you may choose to have them named by the entity name of the referencing entity ignoreing `@Table` annotations. +You activate this behaviour by calling `setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING)` on the `RelationalMappingContext`. + +For `List` and `Map` references an additional column is required for holding the list index or map key. It is based on the foreign key column with an additional `_KEY` suffix. + +If you want a completely different way of naming these back references you may implement `NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)` in a way that fits your needs. + .Declaring and setting an `AggregateReference` ====