Skip to content

Commit b92586f

Browse files
committed
Add expression support for @MappedCollection annotation.
See #1325 Original pull request: #1461
1 parent a73be5a commit b92586f

File tree

6 files changed

+104
-47
lines changed

6 files changed

+104
-47
lines changed

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

+62-27
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.springframework.data.relational.core.sql.SqlIdentifier;
2828
import org.springframework.data.spel.EvaluationContextProvider;
2929
import org.springframework.data.util.Lazy;
30-
import org.springframework.data.util.Optionals;
3130
import org.springframework.expression.Expression;
3231
import org.springframework.expression.ParserContext;
3332
import org.springframework.expression.common.LiteralExpression;
@@ -53,12 +52,14 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
5352
private final Lazy<SqlIdentifier> columnName;
5453
private final @Nullable Expression columnNameExpression;
5554
private final Lazy<Optional<SqlIdentifier>> collectionIdColumnName;
55+
private final @Nullable Expression collectionIdColumnNameExpression;
5656
private final Lazy<SqlIdentifier> collectionKeyColumnName;
57+
private final @Nullable Expression collectionKeyColumnNameExpression;
5758
private final boolean isEmbedded;
5859
private final String embeddedPrefix;
5960
private final NamingStrategy namingStrategy;
6061
private boolean forceQuote = true;
61-
private ExpressionEvaluator spelExpressionProcessor = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT);
62+
private ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT);
6263

6364
/**
6465
* Creates a new {@link BasicRelationalPersistentProperty}.
@@ -99,38 +100,58 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,
99100
.map(Embedded::prefix) //
100101
.orElse("");
101102

103+
Lazy<Optional<SqlIdentifier>> collectionIdColumnName = null;
104+
Lazy<SqlIdentifier> collectionKeyColumnName = Lazy
105+
.of(() -> createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this)));
106+
107+
if (isAnnotationPresent(MappedCollection.class)) {
108+
109+
MappedCollection mappedCollection = getRequiredAnnotation(MappedCollection.class);
110+
111+
if (StringUtils.hasText(mappedCollection.idColumn())) {
112+
collectionIdColumnName = Lazy.of(() -> Optional.of(createSqlIdentifier(mappedCollection.idColumn())));
113+
}
114+
115+
this.collectionIdColumnNameExpression = detectExpression(mappedCollection.idColumn());
116+
117+
collectionKeyColumnName = Lazy.of(
118+
() -> StringUtils.hasText(mappedCollection.keyColumn()) ? createSqlIdentifier(mappedCollection.keyColumn())
119+
: createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this)));
120+
121+
this.collectionKeyColumnNameExpression = detectExpression(mappedCollection.keyColumn());
122+
} else {
123+
124+
this.collectionIdColumnNameExpression = null;
125+
this.collectionKeyColumnNameExpression = null;
126+
}
127+
102128
if (isAnnotationPresent(Column.class)) {
103129

104130
Column column = getRequiredAnnotation(Column.class);
105131

106-
columnName = Lazy.of(() -> StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value())
132+
this.columnName = Lazy.of(() -> StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value())
107133
: createDerivedSqlIdentifier(namingStrategy.getColumnName(this)));
108-
columnNameExpression = detectExpression(column.value());
134+
this.columnNameExpression = detectExpression(column.value());
135+
136+
if (collectionIdColumnName == null && StringUtils.hasText(column.value())) {
137+
collectionIdColumnName = Lazy.of(() -> Optional.of(createSqlIdentifier(column.value())));
138+
}
109139

110140
} else {
111-
columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this)));
112-
columnNameExpression = null;
141+
this.columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this)));
142+
this.columnNameExpression = null;
113143
}
114144

115-
// TODO: support expressions for MappedCollection
116-
this.collectionIdColumnName = Lazy.of(() -> Optionals
117-
.toStream(Optional.ofNullable(findAnnotation(MappedCollection.class)) //
118-
.map(MappedCollection::idColumn), //
119-
Optional.ofNullable(findAnnotation(Column.class)) //
120-
.map(Column::value)) //
121-
.filter(StringUtils::hasText) //
122-
.findFirst() //
123-
.map(this::createSqlIdentifier)); //
124-
125-
this.collectionKeyColumnName = Lazy.of(() -> Optionals //
126-
.toStream(Optional.ofNullable(findAnnotation(MappedCollection.class)).map(MappedCollection::keyColumn)) //
127-
.filter(StringUtils::hasText).findFirst() //
128-
.map(this::createSqlIdentifier) //
129-
.orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this))));
145+
if (collectionIdColumnName == null) {
146+
collectionIdColumnName = Lazy.of(Optional.empty());
147+
}
148+
149+
this.collectionIdColumnName = collectionIdColumnName;
150+
this.collectionKeyColumnName = collectionKeyColumnName;
130151
}
131152

132-
void setSpelExpressionProcessor(ExpressionEvaluator spelExpressionProcessor) {
133-
this.spelExpressionProcessor = spelExpressionProcessor;
153+
void setExpressionEvaluator(ExpressionEvaluator expressionEvaluator) {
154+
this.expressionEvaluator = expressionEvaluator;
134155
}
135156

136157
/**
@@ -184,7 +205,7 @@ public SqlIdentifier getColumnName() {
184205
return columnName.get();
185206
}
186207

187-
return createSqlIdentifier(spelExpressionProcessor.evaluate(columnNameExpression));
208+
return createSqlIdentifier(expressionEvaluator.evaluate(columnNameExpression));
188209
}
189210

190211
@Override
@@ -195,13 +216,27 @@ public RelationalPersistentEntity<?> getOwner() {
195216
@Override
196217
public SqlIdentifier getReverseColumnName(PersistentPropertyPathExtension path) {
197218

198-
return collectionIdColumnName.get()
199-
.orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path)));
219+
if (collectionIdColumnNameExpression == null) {
220+
221+
return collectionIdColumnName.get()
222+
.orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path)));
223+
}
224+
225+
return createSqlIdentifier(expressionEvaluator.evaluate(collectionIdColumnNameExpression));
200226
}
201227

202228
@Override
203229
public SqlIdentifier getKeyColumn() {
204-
return isQualified() ? collectionKeyColumnName.get() : null;
230+
231+
if (!isQualified()) {
232+
return null;
233+
}
234+
235+
if (collectionKeyColumnNameExpression == null) {
236+
return collectionKeyColumnName.get();
237+
}
238+
239+
return createSqlIdentifier(expressionEvaluator.evaluate(collectionKeyColumnNameExpression));
205240
}
206241

207242
@Override

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*
1515
* @author Kurt Niemi
1616
* @see SqlIdentifierSanitizer
17-
* @since 3.1
17+
* @since 3.2
1818
*/
1919
class ExpressionEvaluator {
2020

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,18 @@
3737
public @interface MappedCollection {
3838

3939
/**
40-
* The column name for id column in the corresponding relationship table. Defaults to {@link NamingStrategy} usage if
41-
* the value is empty.
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.
4243
*
4344
* @see NamingStrategy#getReverseColumnName(RelationalPersistentProperty)
4445
*/
4546
String idColumn() default "";
4647

4748
/**
4849
* The column name for key columns of {@link List} or {@link Map} collections in the corresponding relationship table.
49-
* Defaults to {@link NamingStrategy} usage if the value is empty.
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.
5052
*
5153
* @see NamingStrategy#getKeyColumn(RelationalPersistentProperty)
5254
*/

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ public void setForceQuote(boolean forceQuote) {
8383
this.forceQuote = forceQuote;
8484
}
8585

86+
/**
87+
* Set the {@link SqlIdentifierSanitizer} to sanitize
88+
* {@link org.springframework.data.relational.core.sql.SqlIdentifier identifiers} created from SpEL expressions.
89+
*
90+
* @param sanitizer must not be {@literal null}.
91+
* @since 3.2
92+
*/
8693
public void setSqlIdentifierSanitizer(SqlIdentifierSanitizer sanitizer) {
8794
this.expressionEvaluator.setSanitizer(sanitizer);
8895
}
@@ -119,7 +126,7 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert
119126

120127
protected void applyDefaults(BasicRelationalPersistentProperty persistentProperty) {
121128
persistentProperty.setForceQuote(isForceQuote());
122-
persistentProperty.setSpelExpressionProcessor(this.expressionEvaluator);
129+
persistentProperty.setExpressionEvaluator(this.expressionEvaluator);
123130
}
124131

125132
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*
1010
* @author Kurt Niemi
1111
* @author Mark Paluch
12-
* @since 3.1
12+
* @since 3.2
1313
* @see RelationalMappingContext#setSqlIdentifierSanitizer(SqlIdentifierSanitizer)
1414
*/
1515
@FunctionalInterface

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

+27-14
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,24 @@ public void detectsAnnotatedColumnAndKeyName() {
7272
@Test // GH-1325
7373
void testRelationalPersistentEntitySpelExpressions() {
7474

75-
assertThat(entity.getRequiredPersistentProperty("spelExpression1").getColumnName()).isEqualTo(quoted("THE_FORCE_IS_WITH_YOU"));
75+
assertThat(entity.getRequiredPersistentProperty("spelExpression1").getColumnName())
76+
.isEqualTo(quoted("THE_FORCE_IS_WITH_YOU"));
7677
assertThat(entity.getRequiredPersistentProperty("littleBobbyTables").getColumnName())
7778
.isEqualTo(quoted("DROPALLTABLES"));
7879

7980
// Test that sanitizer does affect non-spel expressions
80-
assertThat(entity.getRequiredPersistentProperty(
81-
"poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot").getColumnName())
82-
.isEqualTo(quoted("--; DROP ALL TABLES;--"));
81+
assertThat(entity.getRequiredPersistentProperty("poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot")
82+
.getColumnName()).isEqualTo(quoted("--; DROP ALL TABLES;--"));
83+
}
84+
85+
@Test // GH-1325
86+
void shouldEvaluateMappedCollectionExpressions() {
87+
88+
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(WithMappedCollection.class);
89+
RelationalPersistentProperty property = entity.getRequiredPersistentProperty("someList");
90+
91+
assertThat(property.getKeyColumn()).isEqualTo(quoted("key_col"));
92+
assertThat(property.getReverseColumnName(null)).isEqualTo(quoted("id_col"));
8393
}
8494

8595
@Test // DATAJDBC-111
@@ -166,18 +176,16 @@ private static class DummyEntity {
166176
public static String spelExpression1Value = "THE_FORCE_IS_WITH_YOU";
167177

168178
public static String littleBobbyTablesValue = "--; DROP ALL TABLES;--";
169-
@Column(value="#{T(org.springframework.data.relational.core.mapping." +
170-
"BasicRelationalPersistentPropertyUnitTests$DummyEntity" +
171-
").spelExpression1Value}")
172-
private String spelExpression1;
179+
@Column(value = "#{T(org.springframework.data.relational.core.mapping."
180+
+ "BasicRelationalPersistentPropertyUnitTests$DummyEntity"
181+
+ ").spelExpression1Value}") private String spelExpression1;
173182

174-
@Column(value="#{T(org.springframework.data.relational.core.mapping." +
175-
"BasicRelationalPersistentPropertyUnitTests$DummyEntity" +
176-
").littleBobbyTablesValue}")
177-
private String littleBobbyTables;
183+
@Column(value = "#{T(org.springframework.data.relational.core.mapping."
184+
+ "BasicRelationalPersistentPropertyUnitTests$DummyEntity"
185+
+ ").littleBobbyTablesValue}") private String littleBobbyTables;
178186

179-
@Column(value="--; DROP ALL TABLES;--")
180-
private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot;
187+
@Column(
188+
value = "--; DROP ALL TABLES;--") private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot;
181189

182190
// DATAJDBC-111
183191
private @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddableEntity embeddableEntity;
@@ -199,6 +207,11 @@ public List<Date> getListGetter() {
199207
}
200208
}
201209

210+
static class WithMappedCollection {
211+
212+
@MappedCollection(idColumn = "#{'id_col'}", keyColumn = "#{'key_col'}") private List<Integer> someList;
213+
}
214+
202215
@SuppressWarnings("unused")
203216
private enum SomeEnum {
204217
ALPHA

0 commit comments

Comments
 (0)