From b92586f8c05dad48765df8d89d1b9a7a36e64cf5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 31 May 2023 11:29:51 +0200 Subject: [PATCH] Add expression support for `@MappedCollection` annotation. See #1325 Original pull request: #1461 --- .../BasicRelationalPersistentProperty.java | 89 +++++++++++++------ .../core/mapping/ExpressionEvaluator.java | 2 +- .../core/mapping/MappedCollection.java | 8 +- .../mapping/RelationalMappingContext.java | 9 +- .../core/mapping/SqlIdentifierSanitizer.java | 2 +- ...RelationalPersistentPropertyUnitTests.java | 41 ++++++--- 6 files changed, 104 insertions(+), 47 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index 482775e94..f15abc92e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -27,7 +27,6 @@ import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.Lazy; -import org.springframework.data.util.Optionals; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.common.LiteralExpression; @@ -53,12 +52,14 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent private final Lazy columnName; private final @Nullable Expression columnNameExpression; private final Lazy> collectionIdColumnName; + private final @Nullable Expression collectionIdColumnNameExpression; private final Lazy collectionKeyColumnName; + private final @Nullable Expression collectionKeyColumnNameExpression; private final boolean isEmbedded; private final String embeddedPrefix; private final NamingStrategy namingStrategy; private boolean forceQuote = true; - private ExpressionEvaluator spelExpressionProcessor = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT); + private ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT); /** * Creates a new {@link BasicRelationalPersistentProperty}. @@ -99,38 +100,58 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent .map(Embedded::prefix) // .orElse(""); + Lazy> collectionIdColumnName = null; + Lazy collectionKeyColumnName = Lazy + .of(() -> createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this))); + + if (isAnnotationPresent(MappedCollection.class)) { + + MappedCollection mappedCollection = getRequiredAnnotation(MappedCollection.class); + + if (StringUtils.hasText(mappedCollection.idColumn())) { + collectionIdColumnName = Lazy.of(() -> Optional.of(createSqlIdentifier(mappedCollection.idColumn()))); + } + + this.collectionIdColumnNameExpression = detectExpression(mappedCollection.idColumn()); + + collectionKeyColumnName = Lazy.of( + () -> StringUtils.hasText(mappedCollection.keyColumn()) ? createSqlIdentifier(mappedCollection.keyColumn()) + : createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this))); + + this.collectionKeyColumnNameExpression = detectExpression(mappedCollection.keyColumn()); + } else { + + this.collectionIdColumnNameExpression = null; + this.collectionKeyColumnNameExpression = null; + } + if (isAnnotationPresent(Column.class)) { Column column = getRequiredAnnotation(Column.class); - columnName = Lazy.of(() -> StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value()) + this.columnName = Lazy.of(() -> StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value()) : createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); - columnNameExpression = detectExpression(column.value()); + this.columnNameExpression = detectExpression(column.value()); + + if (collectionIdColumnName == null && StringUtils.hasText(column.value())) { + collectionIdColumnName = Lazy.of(() -> Optional.of(createSqlIdentifier(column.value()))); + } } else { - columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); - columnNameExpression = null; + this.columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); + this.columnNameExpression = null; } - // TODO: support expressions for MappedCollection - this.collectionIdColumnName = Lazy.of(() -> Optionals - .toStream(Optional.ofNullable(findAnnotation(MappedCollection.class)) // - .map(MappedCollection::idColumn), // - Optional.ofNullable(findAnnotation(Column.class)) // - .map(Column::value)) // - .filter(StringUtils::hasText) // - .findFirst() // - .map(this::createSqlIdentifier)); // - - this.collectionKeyColumnName = Lazy.of(() -> Optionals // - .toStream(Optional.ofNullable(findAnnotation(MappedCollection.class)).map(MappedCollection::keyColumn)) // - .filter(StringUtils::hasText).findFirst() // - .map(this::createSqlIdentifier) // - .orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this)))); + if (collectionIdColumnName == null) { + collectionIdColumnName = Lazy.of(Optional.empty()); + } + + this.collectionIdColumnName = collectionIdColumnName; + this.collectionKeyColumnName = collectionKeyColumnName; } - void setSpelExpressionProcessor(ExpressionEvaluator spelExpressionProcessor) { - this.spelExpressionProcessor = spelExpressionProcessor; + void setExpressionEvaluator(ExpressionEvaluator expressionEvaluator) { + this.expressionEvaluator = expressionEvaluator; } /** @@ -184,7 +205,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent return columnName.get(); } - return createSqlIdentifier(spelExpressionProcessor.evaluate(columnNameExpression)); + return createSqlIdentifier(expressionEvaluator.evaluate(columnNameExpression)); } @Override @@ -195,13 +216,27 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent @Override public SqlIdentifier getReverseColumnName(PersistentPropertyPathExtension path) { - return collectionIdColumnName.get() - .orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path))); + if (collectionIdColumnNameExpression == null) { + + return collectionIdColumnName.get() + .orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path))); + } + + return createSqlIdentifier(expressionEvaluator.evaluate(collectionIdColumnNameExpression)); } @Override public SqlIdentifier getKeyColumn() { - return isQualified() ? collectionKeyColumnName.get() : null; + + if (!isQualified()) { + return null; + } + + if (collectionKeyColumnNameExpression == null) { + return collectionKeyColumnName.get(); + } + + return createSqlIdentifier(expressionEvaluator.evaluate(collectionKeyColumnNameExpression)); } @Override diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ExpressionEvaluator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ExpressionEvaluator.java index da4d300ce..28933ddfb 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ExpressionEvaluator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ExpressionEvaluator.java @@ -14,7 +14,7 @@ import org.springframework.util.Assert; * * @author Kurt Niemi * @see SqlIdentifierSanitizer - * @since 3.1 + * @since 3.2 */ class ExpressionEvaluator { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/MappedCollection.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/MappedCollection.java index 10471c549..511ecece5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/MappedCollection.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/MappedCollection.java @@ -37,8 +37,9 @@ import java.util.Set; public @interface MappedCollection { /** - * The column name for id column in the corresponding relationship table. Defaults to {@link NamingStrategy} usage if - * the value is empty. + * The column name for id column in the corresponding relationship table. The attribute supports SpEL expressions to + * dynamically calculate the column name on a per-operation basis. Defaults to {@link NamingStrategy} usage if the + * value is empty. * * @see NamingStrategy#getReverseColumnName(RelationalPersistentProperty) */ @@ -46,7 +47,8 @@ public @interface MappedCollection { /** * The column name for key columns of {@link List} or {@link Map} collections in the corresponding relationship table. - * Defaults to {@link NamingStrategy} usage if the value is empty. + * The attribute supports SpEL expressions to dynamically calculate the column name on a per-operation basis. Defaults + * to {@link NamingStrategy} usage if the value is empty. * * @see NamingStrategy#getKeyColumn(RelationalPersistentProperty) */ 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 966761b74..1c70375cc 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 @@ -83,6 +83,13 @@ public class RelationalMappingContext this.forceQuote = forceQuote; } + /** + * Set the {@link SqlIdentifierSanitizer} to sanitize + * {@link org.springframework.data.relational.core.sql.SqlIdentifier identifiers} created from SpEL expressions. + * + * @param sanitizer must not be {@literal null}. + * @since 3.2 + */ public void setSqlIdentifierSanitizer(SqlIdentifierSanitizer sanitizer) { this.expressionEvaluator.setSanitizer(sanitizer); } @@ -119,7 +126,7 @@ public class RelationalMappingContext protected void applyDefaults(BasicRelationalPersistentProperty persistentProperty) { persistentProperty.setForceQuote(isForceQuote()); - persistentProperty.setSpelExpressionProcessor(this.expressionEvaluator); + persistentProperty.setExpressionEvaluator(this.expressionEvaluator); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SqlIdentifierSanitizer.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SqlIdentifierSanitizer.java index 68b11a09f..9fcfe8ad6 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SqlIdentifierSanitizer.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SqlIdentifierSanitizer.java @@ -9,7 +9,7 @@ import org.springframework.util.Assert; * * @author Kurt Niemi * @author Mark Paluch - * @since 3.1 + * @since 3.2 * @see RelationalMappingContext#setSqlIdentifierSanitizer(SqlIdentifierSanitizer) */ @FunctionalInterface diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java index 2353db437..1ecb663dc 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java @@ -72,14 +72,24 @@ public class BasicRelationalPersistentPropertyUnitTests { @Test // GH-1325 void testRelationalPersistentEntitySpelExpressions() { - assertThat(entity.getRequiredPersistentProperty("spelExpression1").getColumnName()).isEqualTo(quoted("THE_FORCE_IS_WITH_YOU")); + assertThat(entity.getRequiredPersistentProperty("spelExpression1").getColumnName()) + .isEqualTo(quoted("THE_FORCE_IS_WITH_YOU")); assertThat(entity.getRequiredPersistentProperty("littleBobbyTables").getColumnName()) .isEqualTo(quoted("DROPALLTABLES")); // Test that sanitizer does affect non-spel expressions - assertThat(entity.getRequiredPersistentProperty( - "poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot").getColumnName()) - .isEqualTo(quoted("--; DROP ALL TABLES;--")); + assertThat(entity.getRequiredPersistentProperty("poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot") + .getColumnName()).isEqualTo(quoted("--; DROP ALL TABLES;--")); + } + + @Test // GH-1325 + void shouldEvaluateMappedCollectionExpressions() { + + RelationalPersistentEntity entity = context.getRequiredPersistentEntity(WithMappedCollection.class); + RelationalPersistentProperty property = entity.getRequiredPersistentProperty("someList"); + + assertThat(property.getKeyColumn()).isEqualTo(quoted("key_col")); + assertThat(property.getReverseColumnName(null)).isEqualTo(quoted("id_col")); } @Test // DATAJDBC-111 @@ -166,18 +176,16 @@ public class BasicRelationalPersistentPropertyUnitTests { public static String spelExpression1Value = "THE_FORCE_IS_WITH_YOU"; public static String littleBobbyTablesValue = "--; DROP ALL TABLES;--"; - @Column(value="#{T(org.springframework.data.relational.core.mapping." + - "BasicRelationalPersistentPropertyUnitTests$DummyEntity" + - ").spelExpression1Value}") - private String spelExpression1; + @Column(value = "#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentPropertyUnitTests$DummyEntity" + + ").spelExpression1Value}") private String spelExpression1; - @Column(value="#{T(org.springframework.data.relational.core.mapping." + - "BasicRelationalPersistentPropertyUnitTests$DummyEntity" + - ").littleBobbyTablesValue}") - private String littleBobbyTables; + @Column(value = "#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentPropertyUnitTests$DummyEntity" + + ").littleBobbyTablesValue}") private String littleBobbyTables; - @Column(value="--; DROP ALL TABLES;--") - private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot; + @Column( + value = "--; DROP ALL TABLES;--") private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot; // DATAJDBC-111 private @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddableEntity embeddableEntity; @@ -199,6 +207,11 @@ public class BasicRelationalPersistentPropertyUnitTests { } } + static class WithMappedCollection { + + @MappedCollection(idColumn = "#{'id_col'}", keyColumn = "#{'key_col'}") private List someList; + } + @SuppressWarnings("unused") private enum SomeEnum { ALPHA