From cdd989e31949cf5b8490a4aaf61babf020bf22b6 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 1 Sep 2022 14:24:28 +0200 Subject: [PATCH 1/5] 1147-fk-naming - Prepare branch --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-r2dbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) 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-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-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 From 38778f237e478ba8f61714db2744091508cf77ed Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 1 Sep 2022 15:02:55 +0200 Subject: [PATCH 2/5] The back reference generation is now configurable. The default version is the behavior that existed so far: The back reference is the table name as generated by the `NamingStrategy` without taking `@Table` annotations into account. The new alternative is to take `@Table` into account. The behavior can be configured by setting the `foreignKeyNaming` property on the `RelationalMappingContext`. Closes #1161 Closes #1147 --- .../convert/DefaultDataAccessStrategy.java | 4 +- .../data/jdbc/core/convert/SqlContext.java | 2 +- .../data/jdbc/core/convert/SqlGenerator.java | 38 +++++++--- .../jdbc/repository/query/SqlContext.java | 2 +- .../core/convert/SqlGeneratorUnitTests.java | 67 ++++++++++++++++-- .../DefaultReactiveDataAccessStrategy.java | 2 +- .../data/r2dbc/core/R2dbcEntityTemplate.java | 12 ++-- .../core/mapping/CachingNamingStrategy.java | 5 ++ .../core/mapping/DefaultNamingStrategy.java | 69 +++++++++++++++++++ .../core/mapping/ForeignKeyNaming.java | 33 +++++++++ .../core/mapping/NamingStrategy.java | 15 +++- .../mapping/RelationalMappingContext.java | 18 +++++ .../mapping/RelationalPersistentEntity.java | 21 ++++++ .../RelationalPersistentEntityImpl.java | 16 ++++- .../query/SimpleRelationalEntityMetadata.java | 2 +- .../MappingRelationalEntityInformation.java | 2 +- ...lationalPersistentEntityImplUnitTests.java | 34 ++++++--- src/main/asciidoc/jdbc.adoc | 26 +++++-- 18 files changed, 321 insertions(+), 47 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategy.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ForeignKeyNaming.java 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..b0326aec31 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.getFullTableName()); } Column getIdColumn() { 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..b1f44a9f27 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 @@ -201,6 +201,26 @@ String getFindAll(Pageable pageable) { } /** + * 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. + */ + 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 @@ -915,7 +935,7 @@ private SelectBuilder.SelectJoin getSelectCountWithExpression(Expression... coun private SelectBuilder.SelectOrdered applyQueryOnSelect(Query query, MapSqlParameterSource parameterSource, SelectBuilder.SelectWhere selectBuilder) { - Table table = Table.create(this.entity.getTableName()); + Table table = Table.create(this.entity.getFullTableName()); SelectBuilder.SelectOrdered selectOrdered = query // .getCriteria() // @@ -982,15 +1002,15 @@ Column getParentId() { @Override public boolean equals(Object o) { - if (this == 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 (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() { 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..b9559cec4f 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.getFullTableName()); } Column getIdColumn() { 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..653f456f41 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; @@ -41,6 +42,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.DefaultNamingStrategy; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -763,7 +765,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"; @@ -862,6 +864,50 @@ void selectByQueryPaginationValidTest() { .containsOnly(entry("x_name", probe.name)); } + @Test // GH-1161 + void backReferenceShouldConsiderRenamedParent() { + + context.setForeignKeyNaming(APPLY_RENAMING); + + 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() { + + context.setForeignKeyNaming(IGNORE_RENAMING); + + 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() { + + context.setForeignKeyNaming(APPLY_RENAMING); + 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() { + + context.setForeignKeyNaming(IGNORE_RENAMING); + 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 private SqlIdentifier getAlias(Object maybeAliased) { @@ -885,8 +931,7 @@ private PersistentPropertyPath getPath(String path @SuppressWarnings("unused") static class DummyEntity { - @Column("id1") - @Id Long id; + @Column("id1") @Id Long id; String name; ReferencedEntity ref; Set elements; @@ -895,6 +940,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 +990,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); } } @@ -964,8 +1018,7 @@ static class EntityWithQuotedColumnName { // these column names behave like single double quote in the name since the get quoted and then doubling the double // quote escapes it. - @Id - @Column("test\"\"_@id") Long id; + @Id @Column("test\"\"_@id") Long id; @Column("test\"\"_@123") String name; } 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..bd186e13da 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).getFullTableName(); } /* 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..0b04b1adf3 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).getFullTableName()); } 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).getFullTableName()); } 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.getFullTableName(), 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.getFullTableName(), 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).getFullTableName(); } SqlIdentifier getTableNameOrEmpty(Class entityClass) { RelationalPersistentEntity entity = this.mappingContext.getPersistentEntity(entityClass); - return entity != null ? entity.getTableName() : SqlIdentifier.EMPTY; + return entity != null ? entity.getFullTableName() : SqlIdentifier.EMPTY; } private RelationalPersistentEntity getRequiredEntity(Class entityClass) { 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..4cf7d955fd 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 @@ -82,4 +82,9 @@ public String getSchema() { public String getColumnName(RelationalPersistentProperty property) { return columnNames.computeIfAbsent(property, delegate::getColumnName); } + + @Override + public void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) { + delegate.setForeignKeyNaming(foreignKeyNaming); + } } 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..4657a07101 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategy.java @@ -0,0 +1,69 @@ +/* + * 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.jetbrains.annotations.NotNull; +import org.springframework.util.Assert; + +/** + * The default naming strategy used by Spring Data Relational. Names are in SNAKE_CASE. + * + * @author Jens Schauder + * @since 2.4 + */ +public class DefaultNamingStrategy implements NamingStrategy { + + /** + * Since in most cases it doesn't make sense to have more than one {@link NamingStrategy} use of this instance is + * recommended. + */ + public static NamingStrategy INSTANCE = new DefaultNamingStrategy(); + + private ForeignKeyNaming foreignKeyNaming = ForeignKeyNaming.IGNORE_RENAMING; + + @Override + 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().getLeafEntity(); + + return getColumnNameReferencing(leafEntity); + } + + private String getColumnNameReferencing(RelationalPersistentEntity leafEntity) { + + Assert.state(leafEntity != null, "Leaf Entity must not be null."); + + if (foreignKeyNaming == ForeignKeyNaming.IGNORE_RENAMING) { + return getTableName(leafEntity.getType()); + } + + return leafEntity.getSimpleTableName().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..a19a988b14 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,11 @@ 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 = DefaultNamingStrategy.INSTANCE; /** * Defaults to no schema. @@ -82,7 +85,7 @@ default String getReverseColumnName(RelationalPersistentProperty property) { Assert.notNull(property, "Property must not be null"); - return property.getOwner().getTableName().getReference(IdentifierProcessing.NONE); + return property.getOwner().getSimpleTableName().getReference(IdentifierProcessing.NONE); } default String getReverseColumnName(PersistentPropertyPathExtension path) { @@ -102,4 +105,12 @@ default String getKeyColumn(RelationalPersistentProperty property) { return getReverseColumnName(property) + "_key"; } + + /** + * Set the {@link ForeignKeyNaming} strategy used in this {@link NamingStrategy}. + * + * @param foreignKeyNaming the ForeignKeyNaming strategy to be used. Must not be {@literal null}. + * @since 2.4 + */ + default void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) {} } 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..7d8d971608 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 @@ -36,6 +36,7 @@ public class RelationalMappingContext private final NamingStrategy namingStrategy; private boolean forceQuote = true; + private ForeignKeyNaming foreignKeyNaming = ForeignKeyNaming.IGNORE_RENAMING; /** * Creates a new {@link RelationalMappingContext}. @@ -53,6 +54,7 @@ public RelationalMappingContext(NamingStrategy namingStrategy) { Assert.notNull(namingStrategy, "NamingStrategy must not be null"); + namingStrategy.setForeignKeyNaming(foreignKeyNaming); this.namingStrategy = new CachingNamingStrategy(namingStrategy); setSimpleTypeHolder(SimpleTypeHolder.DEFAULT); @@ -101,4 +103,20 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert public NamingStrategy getNamingStrategy() { return this.namingStrategy; } + + /** + * Sets the {@link ForeignKeyNaming} to be used by this mapping context. + * + * @param foreignKeyNaming must not be {@literal null}. + * @since 2.4 + */ + public void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) { + + Assert.notNull(foreignKeyNaming, "foreignKeyNaming must not be null"); + + this.foreignKeyNaming = foreignKeyNaming; + if (namingStrategy != null) { + namingStrategy.setForeignKeyNaming(foreignKeyNaming); + } + } } 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..f5e61976ba 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 @@ -31,13 +31,34 @@ public interface RelationalPersistentEntity extends MutablePersistentEntity explicitlySpecifiedTableName = tableName.get(); - final SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); + SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); if (schema == null) { return explicitlySpecifiedTableName.orElse(schemalessTableIdentifier); @@ -96,6 +101,15 @@ public SqlIdentifier getTableName() { .orElse(SqlIdentifier.from(schema, schemalessTableIdentifier)); } + @Override + public SqlIdentifier getSimpleTableName() { + + Optional explicitlySpecifiedTableName = tableName.get(); + SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); + + return explicitlySpecifiedTableName.orElse(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} 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..c99bf5c8af 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.getFullTableName(); } 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..5bd516f601 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.getFullTableName() : 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..b829349933 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 @@ -42,6 +42,8 @@ public void discoversAnnotatedTableName() { RelationalPersistentEntity entity = mappingContext.getPersistentEntity(DummySubEntity.class); assertThat(entity.getTableName()).isEqualTo(quoted("dummy_sub_entity")); + assertThat(entity.getFullTableName()).isEqualTo(quoted("dummy_sub_entity")); + assertThat(entity.getSimpleTableName()).isEqualTo(quoted("dummy_sub_entity")); } @Test // DATAJDBC-294 @@ -58,6 +60,8 @@ public void emptyTableAnnotationFallsBackToNamingStrategy() { RelationalPersistentEntity entity = mappingContext.getPersistentEntity(DummyEntityWithEmptyAnnotation.class); assertThat(entity.getTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); + assertThat(entity.getFullTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); + assertThat(entity.getSimpleTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); } @Test // DATAJDBC-491 @@ -66,8 +70,16 @@ public void namingStrategyWithSchemaReturnsCompositeTableName() { mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); RelationalPersistentEntity entity = mappingContext.getPersistentEntity(DummyEntityWithEmptyAnnotation.class); + SqlIdentifier simpleExpected = quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION"); + SqlIdentifier fullExpected = SqlIdentifier.from(quoted("MY_SCHEMA"), simpleExpected); + assertThat(entity.getTableName()) - .isEqualTo(SqlIdentifier.from(quoted("MY_SCHEMA"), quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION"))); + .isEqualTo(fullExpected); + assertThat(entity.getFullTableName()) + .isEqualTo(fullExpected); + assertThat(entity.getSimpleTableName()) + .isEqualTo(simpleExpected); + assertThat(entity.getTableName().toSql(IdentifierProcessing.ANSI)) .isEqualTo("\"MY_SCHEMA\".\"DUMMY_ENTITY_WITH_EMPTY_ANNOTATION\""); } @@ -76,21 +88,25 @@ public void namingStrategyWithSchemaReturnsCompositeTableName() { void testRelationalPersistentEntitySchemaNameChoice() { mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); - RelationalPersistentEntity persistentEntity = mappingContext.getPersistentEntity(EntityWithSchemaAndName.class); + RelationalPersistentEntity entity = mappingContext.getPersistentEntity(EntityWithSchemaAndName.class); - SqlIdentifier tableName = persistentEntity.getTableName(); - - 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.getTableName()).isEqualTo(expected); + assertThat(entity.getFullTableName()).isEqualTo(expected); + assertThat(entity.getSimpleTableName()).isEqualTo(simpleExpected); } @Test // GH-1099 void specifiedSchemaGetsCombinedWithNameFromNamingStrategy() { - RelationalPersistentEntity persistentEntity = mappingContext.getPersistentEntity(EntityWithSchema.class); - - SqlIdentifier tableName = persistentEntity.getTableName(); + RelationalPersistentEntity entity = mappingContext.getPersistentEntity(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.getTableName()).isEqualTo(expected); + assertThat(entity.getFullTableName()).isEqualTo(expected); + assertThat(entity.getSimpleTableName()).isEqualTo(simpleExpected); } @Table(schema = "ANAKYN_SKYWALKER") diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index ac78ad9e02..b9107d68fa 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, ignoring any table annotations. + +Alternatively you may choose to have them named by the actual table name of the referencing entity. +You activate this behaviour by calling `setForeignKeyNaming(ForeignKeyNaming.APPLY_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` ==== From e93b1af7a7675f86320d1a8c6630c440915b8b05 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Fri, 2 Sep 2022 11:41:04 +0200 Subject: [PATCH 3/5] New default behaviour for back reference naming. The new default is to take `@Table` annotations into account. The behaviour can be configured by setting the `foreignKeyNaming` property on the `RelationalMappingContext`. Closes #1162 See #1147 --- .../convert/SqlGeneratorFixedNamingStrategyUnitTests.java | 4 ++-- .../jdbc/repository/query/PartTreeJdbcQueryUnitTests.java | 2 +- .../data/relational/core/mapping/DefaultNamingStrategy.java | 2 +- .../relational/core/mapping/RelationalMappingContext.java | 2 +- src/main/asciidoc/jdbc.adoc | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) 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..9ce9c85df2 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 @@ -134,7 +134,7 @@ 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)"); @@ -170,7 +170,7 @@ 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)"); 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-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 index 4657a07101..b282bec617 100644 --- 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 @@ -32,7 +32,7 @@ public class DefaultNamingStrategy implements NamingStrategy { */ public static NamingStrategy INSTANCE = new DefaultNamingStrategy(); - private ForeignKeyNaming foreignKeyNaming = ForeignKeyNaming.IGNORE_RENAMING; + private ForeignKeyNaming foreignKeyNaming = ForeignKeyNaming.APPLY_RENAMING; @Override public void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) { 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 7d8d971608..c3109fdcc8 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 @@ -36,7 +36,7 @@ public class RelationalMappingContext private final NamingStrategy namingStrategy; private boolean forceQuote = true; - private ForeignKeyNaming foreignKeyNaming = ForeignKeyNaming.IGNORE_RENAMING; + private ForeignKeyNaming foreignKeyNaming = ForeignKeyNaming.APPLY_RENAMING; /** * Creates a new {@link RelationalMappingContext}. diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index b9107d68fa..940a48e33b 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -249,10 +249,10 @@ Also, the type of that aggregate is encoded in a type parameter. ==== 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, ignoring any table annotations. +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 actual table name of the referencing entity. -You activate this behaviour by calling `setForeignKeyNaming(ForeignKeyNaming.APPLY_RENAMING)` on the `RelationalMappingContext`. +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. From c9b941ea5774dd4acd3cbee48a02f01ef0949773 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 6 Sep 2022 11:08:36 +0200 Subject: [PATCH 4/5] Polishing. See #1162 --- ...PersistentPropertyPathExtensionUnitTests.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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..62ef1766ea 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.*; @@ -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; From c407ff0da38e1635eca7351946dbc17467d1e7ef Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 5 Oct 2022 11:26:46 +0200 Subject: [PATCH 5/5] Polishing. Do not expose setForeignKeyNaming methods on NamingStrategy to make less assumptions about how a naming strategy gets implemented. Provide getRequiredLeafEntity method on PersistentPropertyPathExtension to reduce null and state assertion checks. Refine getTableName/getQualifiedTableName approach to reduce API surface and avoid deprecations. --- .../jdbc/core/convert/BasicJdbcConverter.java | 7 +- .../data/jdbc/core/convert/SqlContext.java | 4 +- .../data/jdbc/core/convert/SqlGenerator.java | 1821 +++++++++-------- .../jdbc/repository/query/SqlContext.java | 4 +- ...sistentPropertyPathExtensionUnitTests.java | 14 +- ...GeneratorFixedNamingStrategyUnitTests.java | 24 +- .../core/convert/SqlGeneratorUnitTests.java | 58 +- .../DefaultReactiveDataAccessStrategy.java | 2 +- .../data/r2dbc/core/R2dbcEntityTemplate.java | 12 +- .../core/mapping/CachingNamingStrategy.java | 5 - .../core/mapping/DefaultNamingStrategy.java | 18 +- .../core/mapping/NamingStrategy.java | 23 +- .../PersistentPropertyPathExtension.java | 39 +- .../mapping/RelationalMappingContext.java | 19 +- .../mapping/RelationalPersistentEntity.java | 19 +- .../RelationalPersistentEntityImpl.java | 42 +- .../query/SimpleRelationalEntityMetadata.java | 2 +- .../MappingRelationalEntityInformation.java | 2 +- ...lationalPersistentEntityImplUnitTests.java | 56 +- 19 files changed, 1073 insertions(+), 1098 deletions(-) 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/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java index b0326aec31..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.getFullTableName()); + 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 b1f44a9f27..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,147 +60,147 @@ */ 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()); - } - - /** + 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. @@ -208,6 +208,7 @@ String getFindAll(Pageable pageable) { * @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) { @@ -221,789 +222,789 @@ String getFindAllByProperty(Identifier parentIdentifier, } /** - * 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) { + * 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) { - Table table = getTable(); + Assert.isTrue(keyColumn != null || !ordered, + "If the SQL statement should be ordered a keyColumn to order by must be provided"); - List columnExpressions = new ArrayList<>(); + Table table = getTable(); - List joinTables = new ArrayList<>(); - for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { + SelectBuilder.SelectWhere builder = selectBuilder( // + keyColumn == null // + ? Collections.emptyList() // + : Collections.singleton(keyColumn) // + ); - PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); + Condition condition = buildConditionForBackReference(parentIdentifier, table); + SelectBuilder.SelectWhereAndOr withWhereClause = builder.where(condition); - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - joinTables.add(join); - } + Select select = ordered // + ? withWhereClause.orderBy(table.column(keyColumn).as(keyColumn)).build() // + : withWhereClause.build(); - Column column = getColumn(extPath); - if (column != null) { - columnExpressions.add(column); - } - } + return render(select); + } - for (SqlIdentifier keyColumn : keyColumns) { - columnExpressions.add(table.column(keyColumn).as(keyColumn)); - } + private Condition buildConditionForBackReference(Identifier parentIdentifier, Table table) { - SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); - SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); + Condition condition = null; + for (SqlIdentifier backReferenceColumn : parentIdentifier.toMap().keySet()) { - for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } + Condition newCondition = table.column(backReferenceColumn).isEqualTo(getBindMarker(backReferenceColumn)); + condition = condition == null ? newCondition : condition.and(newCondition); + } - return (SelectBuilder.SelectWhere) baseSelect; - } + Assert.state(condition != null, "We need at least one condition"); - private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, - Pageable pageable) { + return condition; + } - SelectBuilder.SelectOrdered sortable = this.selectBuilder(keyColumns); - sortable = applyPagination(pageable, sortable); - return sortable.orderBy(extractOrderByFields(sort)); + /** + * 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(); + } - private SelectBuilder.SelectOrdered applyPagination(Pageable pageable, SelectBuilder.SelectOrdered select) { + /** + * 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); + } - if (!pageable.isPaged()) { - return select; - } + /** + * 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); + } - Assert.isTrue(select instanceof SelectBuilder.SelectLimitOffset, - () -> String.format("Can't apply limit clause to statement of type %s", select.getClass())); + /** + * 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); + } - SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) select; - SelectBuilder.SelectLimitOffset limitResult = limitable.limitOffset(pageable.getPageSize(), pageable.getOffset()); + /** + * Create a {@code UPDATE … SET …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getUpdate() { + return updateSql.get(); + } - 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())); + /** + * 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(); + } - return (SelectBuilder.SelectOrdered) limitResult; - } + /** + * 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 {@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 {@code DELETE FROM … WHERE :id = …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getDeleteById() { + return deleteByIdSql.get(); + } - // 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; - } + /** + * 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(); + } - if (path.isEntity()) { + /** + * 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(); + } - // Simple entities without id include there backreference as a synthetic id in order to distinguish null entities - // from entities with only null values. + /** + * 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(); + } - if (path.isQualified() // - || path.isCollectionLike() // - || path.hasIdProperty() // - ) { - return null; - } + /** + * 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) { - return sqlContext.getReverseColumn(path); - } + Table table = getTable(); - return sqlContext.getColumn(path); - } + DeleteBuilder.DeleteWhere deleteAll = Delete.builder().from(table); - @Nullable - Join getJoin(PersistentPropertyPathExtension path) { + if (path == null) { + return render(deleteAll.build()); + } - if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) { - return null; - } + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), Column::isNotNull); + } - Table currentTable = sqlContext.getTable(path); + /** + * 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))); + } - PersistentPropertyPathExtension idDefiningParentPath = path.getIdDefiningParentPath(); - Table parentTable = sqlContext.getTable(idDefiningParentPath); + /** + * 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 new Join( // - currentTable, // - currentTable.column(path.getReverseColumnName()), // - parentTable.column(idDefiningParentPath.getIdColumnName()) // - ); - } + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), + filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER))); + } - private String createFindAllInListSql() { + private String createFindOneSql() { - Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); + Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .build(); - return render(select); - } + return render(select); + } - private String createExistsSql() { + private String createAcquireLockById(LockMode lockMode) { - Table table = getTable(); + Table table = this.getTable(); - Select select = StatementBuilder // - .select(Functions.count(getIdColumn())) // - .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); + Select select = StatementBuilder // + .select(getIdColumn()) // + .from(table) // + .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .lock(lockMode) // + .build(); - return render(select); - } + return render(select); + } - private String createCountSql() { + private String createAcquireLockAll(LockMode lockMode) { - Table table = getTable(); + Table table = this.getTable(); - Select select = StatementBuilder // - .select(Functions.count(Expressions.asterisk())) // - .from(table) // - .build(); + Select select = StatementBuilder // + .select(getIdColumn()) // + .from(table) // + .lock(lockMode) // + .build(); - return render(select); - } + return render(select); + } - private String createInsertSql(Set additionalColumns) { + private String createFindAllSql() { + return render(selectBuilder().build()); + } - Table table = getTable(); + private SelectBuilder.SelectWhere selectBuilder() { + return selectBuilder(Collections.emptyList()); + } - Set columnNamesForInsert = new TreeSet<>(Comparator.comparing(SqlIdentifier::getReference)); - columnNamesForInsert.addAll(columns.getInsertableColumns()); - columnNamesForInsert.addAll(additionalColumns); + private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns) { - InsertBuilder.InsertIntoColumnsAndValuesWithBuild insert = Insert.builder().into(table); + Table table = getTable(); - for (SqlIdentifier cn : columnNamesForInsert) { - insert = insert.column(table.column(cn)); - } + List columnExpressions = new ArrayList<>(); - if (columnNamesForInsert.isEmpty()) { - return render(insert.build()); - } + List joinTables = new ArrayList<>(); + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), p -> true)) { - InsertBuilder.InsertValuesWithBuild insertWithValues = null; - for (SqlIdentifier cn : columnNamesForInsert) { - insertWithValues = (insertWithValues == null ? insert : insertWithValues).values(getBindMarker(cn)); - } + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); - return render(insertWithValues.build()); - } + // add a join if necessary + Join join = getJoin(extPath); + if (join != null) { + joinTables.add(join); + } - private String createUpdateSql() { - return render(createBaseUpdate().build()); - } + Column column = getColumn(extPath); + if (column != null) { + columnExpressions.add(column); + } + } - private String createUpdateWithVersionSql() { + for (SqlIdentifier keyColumn : keyColumns) { + columnExpressions.add(table.column(keyColumn).as(keyColumn)); + } - Update update = createBaseUpdate() // - .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // - .build(); + SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); + SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); - return render(update); - } + for (Join join : joinTables) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + } - private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() { + return (SelectBuilder.SelectWhere) baseSelect; + } - Table table = getTable(); + private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, + Pageable pageable) { - List assignments = columns.getUpdatableColumns() // - .stream() // - .map(columnName -> Assignments.value( // - table.column(columnName), // - getBindMarker(columnName))) // - .collect(Collectors.toList()); + SelectBuilder.SelectOrdered sortable = this.selectBuilder(keyColumns); + sortable = applyPagination(pageable, sortable); + return sortable.orderBy(extractOrderByFields(sort)); - return Update.builder() // - .table(table) // - .set(assignments) // - .where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))); - } + } - private String createDeleteByIdSql() { - return render(createBaseDeleteById(getTable()).build()); - } + private SelectBuilder.SelectOrdered applyPagination(Pageable pageable, SelectBuilder.SelectOrdered select) { - private String createDeleteByIdInSql() { - return render(createBaseDeleteByIdIn(getTable()).build()); - } + if (!pageable.isPaged()) { + return select; + } - private String createDeleteByIdAndVersionSql() { + Assert.isTrue(select instanceof SelectBuilder.SelectLimitOffset, + () -> String.format("Can't apply limit clause to statement of type %s", select.getClass())); - Delete delete = createBaseDeleteById(getTable()) // - .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // - .build(); + SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) select; + SelectBuilder.SelectLimitOffset limitResult = limitable.limitOffset(pageable.getPageSize(), pageable.getOffset()); - return render(delete); - } + 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())); - private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { + return (SelectBuilder.SelectOrdered) limitResult; + } - return Delete.builder().from(table) - .where(getIdColumn().isEqualTo(SQL.bindMarker(":" + renderReference(ID_SQL_PARAMETER)))); - } + /** + * 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; + } - private DeleteBuilder.DeleteWhereAndOr createBaseDeleteByIdIn(Table table) { + if (path.isEntity()) { - return Delete.builder().from(table) - .where(getIdColumn().in(SQL.bindMarker(":" + renderReference(IDS_SQL_PARAMETER)))); - } + // Simple entities without id include there backreference as a synthetic id in order to distinguish null entities + // from entities with only null values. - private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path, - Function rootCondition) { + if (path.isQualified() // + || path.isCollectionLike() // + || path.hasIdProperty() // + ) { + return null; + } - Table table = Table.create(path.getTableName()); + return sqlContext.getReverseColumn(path); + } - DeleteBuilder.DeleteWhere builder = Delete.builder() // - .from(table); - Delete delete; + return sqlContext.getColumn(path); + } - Column filterColumn = table.column(path.getReverseColumnName()); + @Nullable + Join getJoin(PersistentPropertyPathExtension path) { - if (path.getLength() == 1) { + if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) { + return null; + } - delete = builder // - .where(rootCondition.apply(filterColumn)) // - .build(); - } else { + Table currentTable = sqlContext.getTable(path); - Condition condition = getSubselectCondition(path, rootCondition, filterColumn); - delete = builder.where(condition).build(); - } + PersistentPropertyPathExtension idDefiningParentPath = path.getIdDefiningParentPath(); + Table parentTable = sqlContext.getTable(idDefiningParentPath); - return render(delete); - } + return new Join( // + currentTable, // + currentTable.column(path.getReverseColumnName()), // + parentTable.column(idDefiningParentPath.getIdColumnName()) // + ); + } - private String createDeleteByListSql() { + private String createFindAllInListSql() { - Table table = getTable(); + Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); - Delete delete = Delete.builder() // - .from(table) // - .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))) // - .build(); + return render(select); + } - return render(delete); - } + private String createExistsSql() { - private String render(Select select) { - return this.sqlRenderer.render(select); - } + Table table = getTable(); - private String render(Insert insert) { - return this.sqlRenderer.render(insert); - } + Select select = StatementBuilder // + .select(Functions.count(getIdColumn())) // + .from(table) // + .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .build(); - private String render(Update update) { - return this.sqlRenderer.render(update); - } + return render(select); + } - private String render(Delete delete) { - return this.sqlRenderer.render(delete); - } + private String createCountSql() { - private Table getTable() { - return sqlContext.getTable(); - } + Table table = getTable(); - private Column getIdColumn() { - return sqlContext.getIdColumn(); - } + Select select = StatementBuilder // + .select(Functions.count(Expressions.asterisk())) // + .from(table) // + .build(); - private Column getVersionColumn() { - return sqlContext.getVersionColumn(); - } + return render(select); + } - private String renderReference(SqlIdentifier identifier) { - return identifier.getReference(renderContext.getIdentifierProcessing()); - } + private String createInsertSql(Set additionalColumns) { - private List extractOrderByFields(Sort sort) { + Table table = getTable(); - return sort.stream() // - .map(this::orderToOrderByField) // - .collect(Collectors.toList()); - } + Set columnNamesForInsert = new TreeSet<>(Comparator.comparing(SqlIdentifier::getReference)); + columnNamesForInsert.addAll(columns.getInsertableColumns()); + columnNamesForInsert.addAll(additionalColumns); - private OrderByField orderToOrderByField(Sort.Order order) { + InsertBuilder.InsertIntoColumnsAndValuesWithBuild insert = Insert.builder().into(table); - SqlIdentifier columnName = this.entity.getRequiredPersistentProperty(order.getProperty()).getColumnName(); - Column column = Column.create(columnName, this.getTable()); - return OrderByField.from(column, order.getDirection()).withNullHandling(order.getNullHandling()); - } + for (SqlIdentifier cn : columnNamesForInsert) { + insert = insert.column(table.column(cn)); + } - /** - * 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) { + if (columnNamesForInsert.isEmpty()) { + return render(insert.build()); + } - Assert.notNull(parameterSource, "parameterSource must not be null"); + InsertBuilder.InsertValuesWithBuild insertWithValues = null; + for (SqlIdentifier cn : columnNamesForInsert) { + insertWithValues = (insertWithValues == null ? insert : insertWithValues).values(getBindMarker(cn)); + } - 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.getFullTableName()); - - 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; + return render(insertWithValues.build()); + } + + private String createUpdateSql() { + return render(createBaseUpdate().build()); + } + + private String createUpdateWithVersionSql() { + + Update update = createBaseUpdate() // + .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // + .build(); + + return render(update); + } - 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) { + private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() { + + Table table = getTable(); + + 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()))); + } + + private String createDeleteByIdSql() { + return render(createBaseDeleteById(getTable()).build()); + } + + private String createDeleteByIdInSql() { + return render(createBaseDeleteByIdIn(getTable()).build()); + } + + private String createDeleteByIdAndVersionSql() { + + Delete delete = createBaseDeleteById(getTable()) // + .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // + .build(); + + return render(delete); + } + + private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { + + return Delete.builder().from(table) + .where(getIdColumn().isEqualTo(SQL.bindMarker(":" + renderReference(ID_SQL_PARAMETER)))); + } + + private DeleteBuilder.DeleteWhereAndOr createBaseDeleteByIdIn(Table table) { + + return Delete.builder().from(table) + .where(getIdColumn().in(SQL.bindMarker(":" + renderReference(IDS_SQL_PARAMETER)))); + } + + private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path, + Function rootCondition) { + + Table table = Table.create(path.getQualifiedTableName()); + + DeleteBuilder.DeleteWhere builder = Delete.builder() // + .from(table); + Delete delete; + + Column filterColumn = table.column(path.getReverseColumnName()); + + if (path.getLength() == 1) { + + delete = builder // + .where(rootCondition.apply(filterColumn)) // + .build(); + } else { + + Condition condition = getSubselectCondition(path, rootCondition, filterColumn); + delete = builder.where(condition).build(); + } + + return render(delete); + } + + private String createDeleteByListSql() { + + Table table = getTable(); + + Delete delete = Delete.builder() // + .from(table) // + .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))) // + .build(); + + return render(delete); + } + + private String render(Select select) { + return this.sqlRenderer.render(select); + } + + private String render(Insert insert) { + return this.sqlRenderer.render(insert); + } + + private String render(Update update) { + return this.sqlRenderer.render(update); + } + + private String render(Delete delete) { + return this.sqlRenderer.render(delete); + } + + private Table getTable() { + return sqlContext.getTable(); + } + + private Column getIdColumn() { + return sqlContext.getIdColumn(); + } + + private Column getVersionColumn() { + return sqlContext.getVersionColumn(); + } + + private String renderReference(SqlIdentifier identifier) { + return identifier.getReference(renderContext.getIdentifierProcessing()); + } + + private List extractOrderByFields(Sort sort) { + + return sort.stream() // + .map(this::orderToOrderByField) // + .collect(Collectors.toList()); + } + + 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()); + } + + /** + * 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"); + + 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; + return true; } if (o == null || getClass() != o.getClass()) { return false; @@ -1012,114 +1013,114 @@ public boolean equals(Object 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() { + @Override + public String toString() { - return "Join{" + // - "joinTable=" + joinTable + // - ", joinColumn=" + joinColumn + // - ", parentId=" + parentId + // - '}'; - } - } + return "Join{" + // + "joinTable=" + joinTable + // + ", joinColumn=" + joinColumn + // + ", parentId=" + parentId + // + '}'; + } + } - /** - * Value object encapsulating column name caches. - * - * @author Mark Paluch - * @author Jens Schauder - */ - static class Columns { + /** + * 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 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; + 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) { + Columns(RelationalPersistentEntity entity, + MappingContext, RelationalPersistentProperty> mappingContext, + JdbcConverter converter) { - this.mappingContext = mappingContext; - this.converter = converter; + this.mappingContext = mappingContext; + this.converter = converter; - populateColumnNameCache(entity, ""); + populateColumnNameCache(entity, ""); - Set insertable = new LinkedHashSet<>(nonIdColumnNames); - insertable.removeAll(readOnlyColumnNames); + Set insertable = new LinkedHashSet<>(nonIdColumnNames); + insertable.removeAll(readOnlyColumnNames); - this.insertableColumns = Collections.unmodifiableSet(insertable); + this.insertableColumns = Collections.unmodifiableSet(insertable); - Set updatable = new LinkedHashSet<>(columnNames); + Set updatable = new LinkedHashSet<>(columnNames); - updatable.removeAll(idColumnNames); - updatable.removeAll(readOnlyColumnNames); + updatable.removeAll(idColumnNames); + updatable.removeAll(readOnlyColumnNames); - this.updatableColumns = Collections.unmodifiableSet(updatable); - } + this.updatableColumns = Collections.unmodifiableSet(updatable); + } - private void populateColumnNameCache(RelationalPersistentEntity entity, String prefix) { + private void populateColumnNameCache(RelationalPersistentEntity entity, String prefix) { - entity.doWithAll(property -> { + 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); - } - }); - } + // 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) { + private void initSimpleColumnName(RelationalPersistentProperty property, String prefix) { - SqlIdentifier columnName = property.getColumnName().transform(prefix::concat); + SqlIdentifier columnName = property.getColumnName().transform(prefix::concat); - columnNames.add(columnName); + columnNames.add(columnName); - if (!property.getOwner().isIdProperty(property)) { - nonIdColumnNames.add(columnName); - } else { - idColumnNames.add(columnName); - } + if (!property.getOwner().isIdProperty(property)) { + nonIdColumnNames.add(columnName); + } else { + idColumnNames.add(columnName); + } - if (!property.isWritable()) { - readOnlyColumnNames.add(columnName); - } - } + if (!property.isWritable()) { + readOnlyColumnNames.add(columnName); + } + } - private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { + private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { - String embeddedPrefix = property.getEmbeddedPrefix(); + String embeddedPrefix = property.getEmbeddedPrefix(); - RelationalPersistentEntity embeddedEntity = mappingContext - .getRequiredPersistentEntity(converter.getColumnType(property)); + RelationalPersistentEntity embeddedEntity = mappingContext + .getRequiredPersistentEntity(converter.getColumnType(property)); - populateColumnNameCache(embeddedEntity, prefix + embeddedPrefix); - } + 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 b9559cec4f..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.getFullTableName()); + 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 62ef1766ea..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 @@ -101,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")); }); } 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 9ce9c85df2..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); @@ -141,7 +141,7 @@ public void cascadingDeleteAllSecondLevel() { } @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); @@ -177,7 +177,7 @@ public void cascadingDeleteSecondLevel() { } @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 653f456f41..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 @@ -26,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; @@ -43,7 +44,6 @@ import org.springframework.data.relational.core.dialect.SqlServerDialect; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.DefaultNamingStrategy; -import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -78,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(); }); @@ -749,7 +749,6 @@ void columnForIndirectProperty() { @Test // DATAJDBC-340 void noColumnForReferencedEntity() { - assertThat(generatedColumn("ref", DummyEntity.class)).isNull(); } @@ -778,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 @@ -810,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 @@ -833,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 @@ -860,25 +859,26 @@ 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() { - context.setForeignKeyNaming(APPLY_RENAMING); + 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() { - context.setForeignKeyNaming(IGNORE_RENAMING); + namingStrategy.setForeignKeyNaming(IGNORE_RENAMING); + context = new JdbcMappingContext(namingStrategy); String sql = sqlGenerator.createDeleteInByPath(getPath("ref", RenamedDummy.class)); @@ -888,26 +888,30 @@ void backReferenceShouldIgnoreRenamedParent() { @Test // GH-1161 void keyColumnShouldConsiderRenamedParent() { - context.setForeignKeyNaming(APPLY_RENAMING); + 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)); + 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"); + assertThat(sql).contains("referenced_entity.renamed_key AS renamed_key", "WHERE referenced_entity.parentId"); } @Test // GH-1161 void keyColumnShouldIgnoreRenamedParent() { - context.setForeignKeyNaming(IGNORE_RENAMING); + 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)); + 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"); + assertThat(sql).contains("referenced_entity.renamed_dummy_key AS renamed_dummy_key", + "WHERE referenced_entity.parentId"); } - @Nullable private SqlIdentifier getAlias(Object maybeAliased) { @@ -931,7 +935,8 @@ private PersistentPropertyPath getPath(String path @SuppressWarnings("unused") static class DummyEntity { - @Column("id1") @Id Long id; + @Column("id1") + @Id Long id; String name; ReferencedEntity ref; Set elements; @@ -1018,7 +1023,8 @@ static class EntityWithQuotedColumnName { // these column names behave like single double quote in the name since the get quoted and then doubling the double // quote escapes it. - @Id @Column("test\"\"_@id") Long id; + @Id + @Column("test\"\"_@id") Long id; @Column("test\"\"_@123") String name; } 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 bd186e13da..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).getFullTableName(); + 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 0b04b1adf3..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).getFullTableName()); + 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).getFullTableName()); + 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.getFullTableName(), 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.getFullTableName(), 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).getFullTableName(); + return getRequiredEntity(entityClass).getQualifiedTableName(); } SqlIdentifier getTableNameOrEmpty(Class entityClass) { RelationalPersistentEntity entity = this.mappingContext.getPersistentEntity(entityClass); - return entity != null ? entity.getFullTableName() : SqlIdentifier.EMPTY; + return entity != null ? entity.getQualifiedTableName() : SqlIdentifier.EMPTY; } private RelationalPersistentEntity getRequiredEntity(Class entityClass) { 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 4cf7d955fd..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; @@ -83,8 +82,4 @@ public String getColumnName(RelationalPersistentProperty property) { return columnNames.computeIfAbsent(property, delegate::getColumnName); } - @Override - public void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) { - delegate.setForeignKeyNaming(foreignKeyNaming); - } } 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 index b282bec617..e22cde1d86 100644 --- 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 @@ -15,26 +15,18 @@ */ package org.springframework.data.relational.core.mapping; -import org.jetbrains.annotations.NotNull; import org.springframework.util.Assert; /** - * The default naming strategy used by Spring Data Relational. Names are in SNAKE_CASE. + * The default naming strategy used by Spring Data Relational. Names are in {@code SNAKE_CASE}. * * @author Jens Schauder - * @since 2.4 + * @since 3.0 */ public class DefaultNamingStrategy implements NamingStrategy { - /** - * Since in most cases it doesn't make sense to have more than one {@link NamingStrategy} use of this instance is - * recommended. - */ - public static NamingStrategy INSTANCE = new DefaultNamingStrategy(); - private ForeignKeyNaming foreignKeyNaming = ForeignKeyNaming.APPLY_RENAMING; - @Override public void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) { Assert.notNull(foreignKeyNaming, "foreignKeyNaming must not be null"); @@ -51,19 +43,17 @@ public String getReverseColumnName(RelationalPersistentProperty property) { @Override public String getReverseColumnName(PersistentPropertyPathExtension path) { - RelationalPersistentEntity leafEntity = path.getIdDefiningParentPath().getLeafEntity(); + RelationalPersistentEntity leafEntity = path.getIdDefiningParentPath().getRequiredLeafEntity(); return getColumnNameReferencing(leafEntity); } private String getColumnNameReferencing(RelationalPersistentEntity leafEntity) { - Assert.state(leafEntity != null, "Leaf Entity must not be null."); - if (foreignKeyNaming == ForeignKeyNaming.IGNORE_RENAMING) { return getTableName(leafEntity.getType()); } - return leafEntity.getSimpleTableName().getReference(); + return leafEntity.getTableName().getReference(); } } 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 a19a988b14..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 @@ -41,8 +41,12 @@ public interface NamingStrategy { * * @deprecated use {@link DefaultNamingStrategy#INSTANCE} instead. */ - @Deprecated(since = "2.4") - NamingStrategy INSTANCE = DefaultNamingStrategy.INSTANCE; + @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. @@ -78,19 +82,18 @@ 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) { Assert.notNull(property, "Property must not be null"); - return property.getOwner().getSimpleTableName().getReference(IdentifierProcessing.NONE); + return property.getOwner().getTableName().getReference(IdentifierProcessing.NONE); } default String getReverseColumnName(PersistentPropertyPathExtension path) { - - return getTableName(path.getIdDefiningParentPath().getLeafEntity().getType()); + return getTableName(path.getIdDefiningParentPath().getRequiredLeafEntity().getType()); } /** @@ -105,12 +108,4 @@ default String getKeyColumn(RelationalPersistentProperty property) { return getReverseColumnName(property) + "_key"; } - - /** - * Set the {@link ForeignKeyNaming} strategy used in this {@link NamingStrategy}. - * - * @param foreignKeyNaming the ForeignKeyNaming strategy to be used. Must not be {@literal null}. - * @since 2.4 - */ - default void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) {} } 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 c3109fdcc8..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 @@ -36,13 +36,12 @@ public class RelationalMappingContext private final NamingStrategy namingStrategy; private boolean forceQuote = true; - private ForeignKeyNaming foreignKeyNaming = ForeignKeyNaming.APPLY_RENAMING; /** * Creates a new {@link RelationalMappingContext}. */ public RelationalMappingContext() { - this(NamingStrategy.INSTANCE); + this(new DefaultNamingStrategy()); } /** @@ -54,7 +53,6 @@ public RelationalMappingContext(NamingStrategy namingStrategy) { Assert.notNull(namingStrategy, "NamingStrategy must not be null"); - namingStrategy.setForeignKeyNaming(foreignKeyNaming); this.namingStrategy = new CachingNamingStrategy(namingStrategy); setSimpleTypeHolder(SimpleTypeHolder.DEFAULT); @@ -104,19 +102,4 @@ public NamingStrategy getNamingStrategy() { return this.namingStrategy; } - /** - * Sets the {@link ForeignKeyNaming} to be used by this mapping context. - * - * @param foreignKeyNaming must not be {@literal null}. - * @since 2.4 - */ - public void setForeignKeyNaming(ForeignKeyNaming foreignKeyNaming) { - - Assert.notNull(foreignKeyNaming, "foreignKeyNaming must not be null"); - - this.foreignKeyNaming = foreignKeyNaming; - if (namingStrategy != null) { - namingStrategy.setForeignKeyNaming(foreignKeyNaming); - } - } } 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 f5e61976ba..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,36 +24,27 @@ * * @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. - * @deprecated Use either {@link #getFullTableName()} or {@link #getSimpleTableName()} */ - @Deprecated(since = "2.4") SqlIdentifier getTableName(); /** - * Returns the name of the table backing the given entity, including the schema. + * 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 2.4 + * @since 3.0 */ - default SqlIdentifier getFullTableName() { + default SqlIdentifier getQualifiedTableName() { return getTableName(); } - /** - * Returns the name of the table backing the given entity, without any schema. - * - * @return the table name. - * @since 2.4 - */ - SqlIdentifier getSimpleTableName(); - /** * Returns the column representing the identifier. * 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 4457b7bc0c..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) { @@ -81,11 +76,15 @@ public void setForceQuote(boolean forceQuote) { @Override public SqlIdentifier getTableName() { - return getFullTableName(); + + Optional explicitlySpecifiedTableName = tableName.get(); + SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); + + return explicitlySpecifiedTableName.orElse(schemalessTableIdentifier); } @Override - public SqlIdentifier getFullTableName() { + public SqlIdentifier getQualifiedTableName() { SqlIdentifier schema = determineCurrentEntitySchema(); Optional explicitlySpecifiedTableName = tableName.get(); @@ -96,32 +95,21 @@ public SqlIdentifier getFullTableName() { 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)); } - @Override - public SqlIdentifier getSimpleTableName() { - - Optional explicitlySpecifiedTableName = tableName.get(); - SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); - - return explicitlySpecifiedTableName.orElse(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 c99bf5c8af..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.getFullTableName(); + 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 5bd516f601..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.getFullTableName() : 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 b829349933..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,55 +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.getFullTableName()).isEqualTo(quoted("dummy_sub_entity")); - assertThat(entity.getSimpleTableName()).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.getFullTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); - assertThat(entity.getSimpleTableName()).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.getTableName()) + assertThat(entity.getQualifiedTableName()) .isEqualTo(fullExpected); - assertThat(entity.getFullTableName()) - .isEqualTo(fullExpected); - assertThat(entity.getSimpleTableName()) + assertThat(entity.getTableName()) .isEqualTo(simpleExpected); - assertThat(entity.getTableName().toSql(IdentifierProcessing.ANSI)) + assertThat(entity.getQualifiedTableName().toSql(IdentifierProcessing.ANSI)) .isEqualTo("\"MY_SCHEMA\".\"DUMMY_ENTITY_WITH_EMPTY_ANNOTATION\""); } @@ -88,34 +88,32 @@ public void namingStrategyWithSchemaReturnsCompositeTableName() { void testRelationalPersistentEntitySchemaNameChoice() { mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); - RelationalPersistentEntity entity = mappingContext.getPersistentEntity(EntityWithSchemaAndName.class); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndName.class); SqlIdentifier simpleExpected = quoted("I_AM_THE_SENATE"); SqlIdentifier expected = SqlIdentifier.from(quoted("DART_VADER"), simpleExpected); - assertThat(entity.getTableName()).isEqualTo(expected); - assertThat(entity.getFullTableName()).isEqualTo(expected); - assertThat(entity.getSimpleTableName()).isEqualTo(simpleExpected); + assertThat(entity.getQualifiedTableName()).isEqualTo(expected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); } @Test // GH-1099 void specifiedSchemaGetsCombinedWithNameFromNamingStrategy() { - RelationalPersistentEntity entity = mappingContext.getPersistentEntity(EntityWithSchema.class); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchema.class); SqlIdentifier simpleExpected = quoted("ENTITY_WITH_SCHEMA"); SqlIdentifier expected = SqlIdentifier.from(quoted("ANAKYN_SKYWALKER"), simpleExpected); - assertThat(entity.getTableName()).isEqualTo(expected); - assertThat(entity.getFullTableName()).isEqualTo(expected); - assertThat(entity.getSimpleTableName()).isEqualTo(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; }