Skip to content

Commit 0581a6e

Browse files
committed
Polishing.
Switch to value expressions to allow property-placeholder usage. Add tests. Add missing support for MappedCollection.idColumn() expressions. See #1524 Original pull request: #2077
1 parent 9d4efca commit 0581a6e

File tree

8 files changed

+122
-37
lines changed

8 files changed

+122
-37
lines changed

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717

1818
import java.util.Optional;
1919

20+
import org.springframework.data.expression.ValueExpression;
21+
import org.springframework.data.expression.ValueExpressionParser;
2022
import org.springframework.data.mapping.model.BasicPersistentEntity;
2123
import org.springframework.data.relational.core.sql.SqlIdentifier;
2224
import org.springframework.data.util.Lazy;
2325
import org.springframework.data.util.TypeInformation;
2426
import org.springframework.expression.Expression;
25-
import org.springframework.expression.ParserContext;
2627
import org.springframework.expression.common.LiteralExpression;
27-
import org.springframework.expression.spel.standard.SpelExpressionParser;
2828
import org.springframework.lang.Nullable;
2929
import org.springframework.util.StringUtils;
3030

@@ -38,16 +38,17 @@
3838
* @author Mikhail Polivakha
3939
* @author Kurt Niemi
4040
* @author Sergey Korotaev
41+
* @author Mark Paluch
4142
*/
4243
class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, RelationalPersistentProperty>
4344
implements RelationalPersistentEntity<T> {
4445

45-
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
46+
private static final ValueExpressionParser PARSER = ValueExpressionParser.create();
4647

4748
private final Lazy<SqlIdentifier> tableName;
48-
private final @Nullable Expression tableNameExpression;
49+
private final @Nullable ValueExpression tableNameExpression;
4950
private final Lazy<Optional<SqlIdentifier>> schemaName;
50-
private final @Nullable Expression schemaNameExpression;
51+
private final @Nullable ValueExpression schemaNameExpression;
5152
private final SqlIdentifierExpressionEvaluator sqlIdentifierExpressionEvaluator;
5253
private boolean forceQuote = true;
5354

@@ -97,14 +98,14 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati
9798
* @return can be {@literal null}.
9899
*/
99100
@Nullable
100-
private static Expression detectExpression(@Nullable String potentialExpression) {
101+
private static ValueExpression detectExpression(@Nullable String potentialExpression) {
101102

102103
if (!StringUtils.hasText(potentialExpression)) {
103104
return null;
104105
}
105106

106-
Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION);
107-
return expression instanceof LiteralExpression ? null : expression;
107+
ValueExpression expression = PARSER.parse(potentialExpression);
108+
return expression.isLiteral() ? null : expression;
108109
}
109110

110111
private SqlIdentifier createSqlIdentifier(String name) {

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import java.util.Optional;
1919
import java.util.Set;
2020

21+
import org.springframework.data.expression.ValueExpression;
22+
import org.springframework.data.expression.ValueExpressionParser;
2123
import org.springframework.data.mapping.Association;
2224
import org.springframework.data.mapping.PersistentEntity;
2325
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
@@ -28,9 +30,7 @@
2830
import org.springframework.data.spel.EvaluationContextProvider;
2931
import org.springframework.data.util.Lazy;
3032
import org.springframework.expression.Expression;
31-
import org.springframework.expression.ParserContext;
3233
import org.springframework.expression.common.LiteralExpression;
33-
import org.springframework.expression.spel.standard.SpelExpressionParser;
3434
import org.springframework.lang.Nullable;
3535
import org.springframework.util.Assert;
3636
import org.springframework.util.StringUtils;
@@ -44,19 +44,21 @@
4444
* @author Bastian Wilhelm
4545
* @author Kurt Niemi
4646
* @author Sergey Korotaev
47+
* @author Mark Paluch
4748
*/
4849
public class BasicRelationalPersistentProperty extends AnnotationBasedPersistentProperty<RelationalPersistentProperty>
4950
implements RelationalPersistentProperty {
5051

51-
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
52+
private static final ValueExpressionParser PARSER = ValueExpressionParser.create();
5253

5354
private final Lazy<SqlIdentifier> columnName;
5455
private final boolean hasExplicitColumnName;
55-
private final @Nullable Expression columnNameExpression;
56+
private final @Nullable ValueExpression columnNameExpression;
5657
private final SqlIdentifier sequence;
5758
private final Lazy<Optional<SqlIdentifier>> collectionIdColumnName;
59+
private final @Nullable ValueExpression collectionIdColumnNameExpression;
5860
private final Lazy<SqlIdentifier> collectionKeyColumnName;
59-
private final @Nullable Expression collectionKeyColumnNameExpression;
61+
private final @Nullable ValueExpression collectionKeyColumnNameExpression;
6062
private final boolean isEmbedded;
6163
private final String embeddedPrefix;
6264

@@ -99,6 +101,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,
99101
if (StringUtils.hasText(mappedCollection.idColumn())) {
100102
collectionIdColumnName = Lazy.of(() -> Optional.of(createSqlIdentifier(mappedCollection.idColumn())));
101103
}
104+
this.collectionIdColumnNameExpression = detectExpression(mappedCollection.idColumn());
102105

103106
collectionKeyColumnName = Lazy.of(
104107
() -> StringUtils.hasText(mappedCollection.keyColumn()) ? createSqlIdentifier(mappedCollection.keyColumn())
@@ -107,6 +110,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,
107110
this.collectionKeyColumnNameExpression = detectExpression(mappedCollection.keyColumn());
108111
} else {
109112

113+
this.collectionIdColumnNameExpression = null;
110114
this.collectionKeyColumnNameExpression = null;
111115
}
112116

@@ -151,14 +155,14 @@ void setSqlIdentifierExpressionEvaluator(SqlIdentifierExpressionEvaluator sqlIde
151155
* @return can be {@literal null}.
152156
*/
153157
@Nullable
154-
private static Expression detectExpression(@Nullable String potentialExpression) {
158+
private static ValueExpression detectExpression(@Nullable String potentialExpression) {
155159

156160
if (!StringUtils.hasText(potentialExpression)) {
157161
return null;
158162
}
159163

160-
Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION);
161-
return expression instanceof LiteralExpression ? null : expression;
164+
ValueExpression expression = PARSER.parse(potentialExpression);
165+
return expression.isLiteral() ? null : expression;
162166
}
163167

164168
private SqlIdentifier createSqlIdentifier(String name) {
@@ -210,8 +214,13 @@ public RelationalPersistentEntity<?> getOwner() {
210214
@Override
211215
public SqlIdentifier getReverseColumnName(RelationalPersistentEntity<?> owner) {
212216

213-
return collectionIdColumnName.get()
214-
.orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(owner)));
217+
if (collectionIdColumnNameExpression == null) {
218+
219+
return collectionIdColumnName.get()
220+
.orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(owner)));
221+
}
222+
223+
return sqlIdentifierExpressionEvaluator.evaluate(collectionIdColumnNameExpression, isForceQuote());
215224
}
216225

217226
@Override

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,18 @@
2727
* @author Kazuki Shimizu
2828
* @author Florian Lüdiger
2929
* @author Bastian Wilhelm
30+
* @author Mark Paluch
3031
*/
3132
@Retention(RetentionPolicy.RUNTIME)
3233
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
3334
@Documented
3435
public @interface Column {
3536

3637
/**
37-
* The column name. The attribute supports SpEL expressions to dynamically calculate the column name on a
38-
* per-operation basis.
38+
* The column name. The attribute supports Value Expressions to dynamically obtain the column name on a per-operation
39+
* basis. Expressions returning a {@code String} are {@link SqlIdentifierSanitizer sanitized} prior usage. Expressions
40+
* can also return a {@link org.springframework.data.relational.core.sql.SqlIdentifier} directly that is used as-is
41+
* without further sanitization.
3942
*/
4043
String value() default "";
4144

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,36 @@
2727
/**
2828
* The annotation to configure the mapping for a {@link List}, {@link Set} or {@link Map} property in the database.
2929
*
30-
* @since 1.1
3130
* @author Bastian Wilhelm
3231
* @author Mark Paluch
32+
* @since 1.1
3333
*/
3434
@Retention(RetentionPolicy.RUNTIME)
3535
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
3636
@Documented
3737
public @interface MappedCollection {
3838

3939
/**
40-
* The column name for id column in the corresponding relationship table. The attribute supports SpEL expressions to
41-
* dynamically calculate the column name on a per-operation basis. Defaults to {@link NamingStrategy} usage if the
42-
* value is empty.
40+
* The column name for id column in the corresponding relationship table. The attribute supports Value Expressions to
41+
* dynamically obtain the column name on a per-operation basis. Defaults to {@link NamingStrategy} usage if the value
42+
* is empty. Expressions returning a {@code String} are {@link SqlIdentifierSanitizer sanitized} prior usage.
43+
* Expressions can also return a {@link org.springframework.data.relational.core.sql.SqlIdentifier} directly that is
44+
* used as-is without further sanitization.
4345
*
4446
* @see NamingStrategy#getReverseColumnName(RelationalPersistentProperty)
4547
*/
4648
String idColumn() default "";
4749

4850
/**
4951
* The column name for key columns of {@link List} or {@link Map} collections in the corresponding relationship table.
50-
* The attribute supports SpEL expressions to dynamically calculate the column name on a per-operation basis. Defaults
51-
* to {@link NamingStrategy} usage if the value is empty.
52+
* The attribute supports Value Expressions to dynamically obtain the column name on a per-operation basis. Defaults
53+
* to {@link NamingStrategy} usage if the value is empty. Expressions returning a {@code String} are
54+
* {@link SqlIdentifierSanitizer sanitized} prior usage. Expressions can also return a
55+
* {@link org.springframework.data.relational.core.sql.SqlIdentifier} directly that is used as-is without further
56+
* sanitization.
5257
*
5358
* @see NamingStrategy#getKeyColumn(RelationalPersistentProperty)
5459
*/
5560
String keyColumn() default "";
61+
5662
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.beans.BeansException;
2222
import org.springframework.context.ApplicationContext;
23+
import org.springframework.core.env.Environment;
2324
import org.springframework.data.mapping.PersistentPropertyPath;
2425
import org.springframework.data.mapping.context.AbstractMappingContext;
2526
import org.springframework.data.mapping.context.MappingContext;
@@ -112,6 +113,12 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
112113
this.sqlIdentifierExpressionEvaluator.setProvider(new ExtensionAwareEvaluationContextProvider(applicationContext));
113114
}
114115

116+
@Override
117+
public void setEnvironment(Environment environment) {
118+
this.sqlIdentifierExpressionEvaluator.setEnvironment(environment);
119+
super.setEnvironment(environment);
120+
}
121+
115122
@Nullable
116123
@Override
117124
public RelationalPersistentEntity<?> getPersistentEntity(RelationalPersistentProperty persistentProperty) {

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package org.springframework.data.relational.core.mapping;
22

3+
import org.springframework.core.env.Environment;
4+
import org.springframework.core.env.StandardEnvironment;
5+
import org.springframework.data.expression.ValueEvaluationContext;
6+
import org.springframework.data.expression.ValueExpression;
37
import org.springframework.data.relational.core.sql.SqlIdentifier;
48
import org.springframework.data.spel.EvaluationContextProvider;
9+
import org.springframework.expression.EvaluationContext;
510
import org.springframework.expression.EvaluationException;
6-
import org.springframework.expression.Expression;
711
import org.springframework.util.Assert;
812

913
/**
@@ -15,6 +19,7 @@
1519
*
1620
* @author Kurt Niemi
1721
* @author Sergey Korotaev
22+
* @author Mark Paluch
1823
* @see SqlIdentifierSanitizer
1924
* @since 3.2
2025
*/
@@ -23,21 +28,29 @@ class SqlIdentifierExpressionEvaluator {
2328
private EvaluationContextProvider provider;
2429

2530
private SqlIdentifierSanitizer sanitizer = SqlIdentifierSanitizer.words();
31+
private Environment environment = new StandardEnvironment();
2632

2733
public SqlIdentifierExpressionEvaluator(EvaluationContextProvider provider) {
2834
this.provider = provider;
2935
}
3036

31-
public SqlIdentifier evaluate(Expression expression, boolean isForceQuote) throws EvaluationException {
37+
public SqlIdentifier evaluate(ValueExpression expression, boolean isForceQuote) throws EvaluationException {
3238

3339
Assert.notNull(expression, "Expression must not be null.");
3440

35-
Object value = expression.getValue(provider.getEvaluationContext(null), Object.class);
41+
EvaluationContext evaluationContext = provider.getEvaluationContext(null);
42+
ValueEvaluationContext valueEvaluationContext = ValueEvaluationContext.of(environment, evaluationContext);
43+
44+
Object value = expression.evaluate(valueEvaluationContext);
3645
if (value instanceof SqlIdentifier sqlIdentifier) {
3746
return sqlIdentifier;
3847
}
3948

40-
String sanitizedResult = sanitizer.sanitize((String) value);
49+
if (value == null) {
50+
throw new EvaluationException("Expression '%s' evaluated to 'null'".formatted(expression));
51+
}
52+
53+
String sanitizedResult = sanitizer.sanitize(value.toString());
4154
return isForceQuote ? SqlIdentifier.quoted(sanitizedResult) : SqlIdentifier.unquoted(sanitizedResult);
4255
}
4356

@@ -49,6 +62,16 @@ public void setSanitizer(SqlIdentifierSanitizer sanitizer) {
4962
}
5063

5164
public void setProvider(EvaluationContextProvider provider) {
65+
66+
Assert.notNull(provider, "EvaluationContextProvider must not be null");
67+
5268
this.provider = provider;
5369
}
70+
71+
public void setEnvironment(Environment environment) {
72+
73+
Assert.notNull(environment, "Environment must not be null");
74+
75+
this.environment = environment;
76+
}
5477
}

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* @author Kazuki Shimizu
3131
* @author Bastian Wilhelm
3232
* @author Mikhail Polivakha
33+
* @author Mark Paluch
3334
*/
3435
@Retention(RetentionPolicy.RUNTIME)
3536
@Target(ElementType.TYPE)
@@ -38,26 +39,35 @@
3839
public @interface Table {
3940

4041
/**
41-
* The table name. The attribute supports SpEL expressions to dynamically calculate the table name on a per-operation
42-
* basis.
42+
* The table name. The attribute supports value expressions to dynamically obtain the table name on a per-operation
43+
* basis. Expressions returning a {@code String} are {@link SqlIdentifierSanitizer sanitized} prior usage. Expressions
44+
* can also return a {@link org.springframework.data.relational.core.sql.SqlIdentifier} directly that is used as-is
45+
* without further sanitization.
4346
*/
4447
@AliasFor("name")
4548
String value() default "";
4649

4750
/**
48-
* The table name. The attribute supports SpEL expressions to dynamically calculate the table name on a per-operation
49-
* basis.
51+
* The table name. The attribute supports value expressions to dynamically obtain the table name on a per-operation
52+
* basis. Expressions returning a {@code String} are {@link SqlIdentifierSanitizer sanitized} prior usage. Expressions
53+
* can also return a {@link org.springframework.data.relational.core.sql.SqlIdentifier} directly that is used as-is
54+
* without further sanitization.
5055
*/
5156
@AliasFor("value")
5257
String name() default "";
5358

5459
/**
55-
* Name of the schema (or user, for example in case of oracle), in which this table resides in The behavior is the
60+
* Name of the schema (or user, for example in case of Oracle), in which this table resides in The behavior is the
5661
* following: <br/>
5762
* If the {@link Table#schema()} is specified, then it will be used as a schema of current table, i.e. as a prefix to
5863
* the name of the table, which can be specified in {@link Table#value()}. <br/>
5964
* If the {@link Table#schema()} is not specified, then spring data will assume the default schema, The default schema
60-
* itself can be provided by the means of {@link NamingStrategy#getSchema()}
65+
* itself can be provided by the means of {@link NamingStrategy#getSchema()}. The attribute supports value expressions
66+
* to dynamically obtain the schema name on a per-operation basis. Expressions returning a {@code String} are
67+
* {@link SqlIdentifierSanitizer sanitized} prior usage. Expressions can also return a
68+
* {@link org.springframework.data.relational.core.sql.SqlIdentifier} directly that is used as-is without further
69+
* sanitization.
6170
*/
6271
String schema() default "";
72+
6373
}

0 commit comments

Comments
 (0)