Browse Source

Add expression support for `@MappedCollection` annotation.

See #1325
Original pull request: #1461
pull/1526/head
Mark Paluch 3 years ago
parent
commit
b92586f8c0
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 89
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java
  2. 2
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ExpressionEvaluator.java
  3. 8
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/MappedCollection.java
  4. 9
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java
  5. 2
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SqlIdentifierSanitizer.java
  6. 41
      spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java

89
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.relational.core.sql.SqlIdentifier;
import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.util.Lazy; import org.springframework.data.util.Lazy;
import org.springframework.data.util.Optionals;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext; import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.common.LiteralExpression;
@ -53,12 +52,14 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
private final Lazy<SqlIdentifier> columnName; private final Lazy<SqlIdentifier> columnName;
private final @Nullable Expression columnNameExpression; private final @Nullable Expression columnNameExpression;
private final Lazy<Optional<SqlIdentifier>> collectionIdColumnName; private final Lazy<Optional<SqlIdentifier>> collectionIdColumnName;
private final @Nullable Expression collectionIdColumnNameExpression;
private final Lazy<SqlIdentifier> collectionKeyColumnName; private final Lazy<SqlIdentifier> collectionKeyColumnName;
private final @Nullable Expression collectionKeyColumnNameExpression;
private final boolean isEmbedded; private final boolean isEmbedded;
private final String embeddedPrefix; private final String embeddedPrefix;
private final NamingStrategy namingStrategy; private final NamingStrategy namingStrategy;
private boolean forceQuote = true; private boolean forceQuote = true;
private ExpressionEvaluator spelExpressionProcessor = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT); private ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT);
/** /**
* Creates a new {@link BasicRelationalPersistentProperty}. * Creates a new {@link BasicRelationalPersistentProperty}.
@ -99,38 +100,58 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
.map(Embedded::prefix) // .map(Embedded::prefix) //
.orElse(""); .orElse("");
Lazy<Optional<SqlIdentifier>> collectionIdColumnName = null;
Lazy<SqlIdentifier> 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)) { if (isAnnotationPresent(Column.class)) {
Column column = getRequiredAnnotation(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))); : 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 { } else {
columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); this.columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this)));
columnNameExpression = null; this.columnNameExpression = null;
} }
// TODO: support expressions for MappedCollection if (collectionIdColumnName == null) {
this.collectionIdColumnName = Lazy.of(() -> Optionals collectionIdColumnName = Lazy.of(Optional.empty());
.toStream(Optional.ofNullable(findAnnotation(MappedCollection.class)) // }
.map(MappedCollection::idColumn), //
Optional.ofNullable(findAnnotation(Column.class)) // this.collectionIdColumnName = collectionIdColumnName;
.map(Column::value)) // this.collectionKeyColumnName = collectionKeyColumnName;
.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))));
} }
void setSpelExpressionProcessor(ExpressionEvaluator spelExpressionProcessor) { void setExpressionEvaluator(ExpressionEvaluator expressionEvaluator) {
this.spelExpressionProcessor = spelExpressionProcessor; this.expressionEvaluator = expressionEvaluator;
} }
/** /**
@ -184,7 +205,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
return columnName.get(); return columnName.get();
} }
return createSqlIdentifier(spelExpressionProcessor.evaluate(columnNameExpression)); return createSqlIdentifier(expressionEvaluator.evaluate(columnNameExpression));
} }
@Override @Override
@ -195,13 +216,27 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
@Override @Override
public SqlIdentifier getReverseColumnName(PersistentPropertyPathExtension path) { public SqlIdentifier getReverseColumnName(PersistentPropertyPathExtension path) {
return collectionIdColumnName.get() if (collectionIdColumnNameExpression == null) {
.orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path)));
return collectionIdColumnName.get()
.orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path)));
}
return createSqlIdentifier(expressionEvaluator.evaluate(collectionIdColumnNameExpression));
} }
@Override @Override
public SqlIdentifier getKeyColumn() { public SqlIdentifier getKeyColumn() {
return isQualified() ? collectionKeyColumnName.get() : null;
if (!isQualified()) {
return null;
}
if (collectionKeyColumnNameExpression == null) {
return collectionKeyColumnName.get();
}
return createSqlIdentifier(expressionEvaluator.evaluate(collectionKeyColumnNameExpression));
} }
@Override @Override

2
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 * @author Kurt Niemi
* @see SqlIdentifierSanitizer * @see SqlIdentifierSanitizer
* @since 3.1 * @since 3.2
*/ */
class ExpressionEvaluator { class ExpressionEvaluator {

8
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 { public @interface MappedCollection {
/** /**
* The column name for id column in the corresponding relationship table. Defaults to {@link NamingStrategy} usage if * The column name for id column in the corresponding relationship table. The attribute supports SpEL expressions to
* the value is empty. * dynamically calculate the column name on a per-operation basis. Defaults to {@link NamingStrategy} usage if the
* value is empty.
* *
* @see NamingStrategy#getReverseColumnName(RelationalPersistentProperty) * @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. * 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) * @see NamingStrategy#getKeyColumn(RelationalPersistentProperty)
*/ */

9
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java

@ -83,6 +83,13 @@ public class RelationalMappingContext
this.forceQuote = forceQuote; 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) { public void setSqlIdentifierSanitizer(SqlIdentifierSanitizer sanitizer) {
this.expressionEvaluator.setSanitizer(sanitizer); this.expressionEvaluator.setSanitizer(sanitizer);
} }
@ -119,7 +126,7 @@ public class RelationalMappingContext
protected void applyDefaults(BasicRelationalPersistentProperty persistentProperty) { protected void applyDefaults(BasicRelationalPersistentProperty persistentProperty) {
persistentProperty.setForceQuote(isForceQuote()); persistentProperty.setForceQuote(isForceQuote());
persistentProperty.setSpelExpressionProcessor(this.expressionEvaluator); persistentProperty.setExpressionEvaluator(this.expressionEvaluator);
} }
} }

2
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 Kurt Niemi
* @author Mark Paluch * @author Mark Paluch
* @since 3.1 * @since 3.2
* @see RelationalMappingContext#setSqlIdentifierSanitizer(SqlIdentifierSanitizer) * @see RelationalMappingContext#setSqlIdentifierSanitizer(SqlIdentifierSanitizer)
*/ */
@FunctionalInterface @FunctionalInterface

41
spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java

@ -72,14 +72,24 @@ public class BasicRelationalPersistentPropertyUnitTests {
@Test // GH-1325 @Test // GH-1325
void testRelationalPersistentEntitySpelExpressions() { 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()) assertThat(entity.getRequiredPersistentProperty("littleBobbyTables").getColumnName())
.isEqualTo(quoted("DROPALLTABLES")); .isEqualTo(quoted("DROPALLTABLES"));
// Test that sanitizer does affect non-spel expressions // Test that sanitizer does affect non-spel expressions
assertThat(entity.getRequiredPersistentProperty( assertThat(entity.getRequiredPersistentProperty("poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot")
"poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot").getColumnName()) .getColumnName()).isEqualTo(quoted("--; DROP ALL TABLES;--"));
.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 @Test // DATAJDBC-111
@ -166,18 +176,16 @@ public class BasicRelationalPersistentPropertyUnitTests {
public static String spelExpression1Value = "THE_FORCE_IS_WITH_YOU"; public static String spelExpression1Value = "THE_FORCE_IS_WITH_YOU";
public static String littleBobbyTablesValue = "--; DROP ALL TABLES;--"; public static String littleBobbyTablesValue = "--; DROP ALL TABLES;--";
@Column(value="#{T(org.springframework.data.relational.core.mapping." + @Column(value = "#{T(org.springframework.data.relational.core.mapping."
"BasicRelationalPersistentPropertyUnitTests$DummyEntity" + + "BasicRelationalPersistentPropertyUnitTests$DummyEntity"
").spelExpression1Value}") + ").spelExpression1Value}") private String spelExpression1;
private String spelExpression1;
@Column(value="#{T(org.springframework.data.relational.core.mapping." + @Column(value = "#{T(org.springframework.data.relational.core.mapping."
"BasicRelationalPersistentPropertyUnitTests$DummyEntity" + + "BasicRelationalPersistentPropertyUnitTests$DummyEntity"
").littleBobbyTablesValue}") + ").littleBobbyTablesValue}") private String littleBobbyTables;
private String littleBobbyTables;
@Column(value="--; DROP ALL TABLES;--") @Column(
private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot; value = "--; DROP ALL TABLES;--") private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot;
// DATAJDBC-111 // DATAJDBC-111
private @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddableEntity embeddableEntity; 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<Integer> someList;
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
private enum SomeEnum { private enum SomeEnum {
ALPHA ALPHA

Loading…
Cancel
Save