DATAMONGO-2112 - Allow SpEL expression to be used for annotated index & geoIndex names as well as compound index definition.
We now also evaluate SpEL expressions for the name of indices as well as the def attribute of the compound index definition.
@CompoundIndex(name = "#{'cmp' + 2 + 'name‘}“, def = "#{T(org.bson.Document).parse(\"{ 'foo': 1, 'bar': -1 }\")}")
class WithCompoundIndexFromExpression {
// …
}
An expression used for Indexed.expireAfter may now not only return a plain String value with the timeout but also a java.time.Duration.
Original pull request: #647.
This commit is contained in:
committed by
Mark Paluch
parent
82da8027f5
commit
8fbaba0a7c
@@ -36,10 +36,29 @@ import java.lang.annotation.Target;
|
||||
public @interface CompoundIndex {
|
||||
|
||||
/**
|
||||
* The actual index definition in JSON format. The keys of the JSON document are the fields to be indexed, the values
|
||||
* define the index direction (1 for ascending, -1 for descending). <br />
|
||||
* The actual index definition in JSON format or a {@link org.springframework.expression.spel.standard.SpelExpression
|
||||
* template expression} resolving to either a JSON String or a {@link org.bson.Document}. The keys of the JSON
|
||||
* document are the fields to be indexed, the values define the index direction (1 for ascending, -1 for descending).
|
||||
* <br />
|
||||
* If left empty on nested document, the whole document will be indexed.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
*
|
||||
* @Document
|
||||
* @CompoundIndex(def = "{'h1': 1, 'h2': 1}")
|
||||
* class JsonStringIndexDefinition {
|
||||
* String h1, h2;
|
||||
* }
|
||||
*
|
||||
* @Document
|
||||
* @CompoundIndex(def = "#{T(org.bson.Document).parse("{ 'h1': 1, 'h2': 1 }")}")
|
||||
* class ExpressionIndexDefinition {
|
||||
* String h1, h2;
|
||||
* }
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String def() default "";
|
||||
@@ -79,7 +98,8 @@ public @interface CompoundIndex {
|
||||
boolean dropDups() default false;
|
||||
|
||||
/**
|
||||
* The name of the index to be created. <br />
|
||||
* Index name of the index to be created either as plain value or as
|
||||
* {@link org.springframework.expression.spel.standard.SpelExpression template expression}. <br />
|
||||
* <br />
|
||||
* The name will only be applied as is when defined on root level. For usage on nested or embedded structures the
|
||||
* provided name will be prefixed with the path leading to the entity. <br />
|
||||
|
||||
@@ -34,8 +34,8 @@ import java.lang.annotation.Target;
|
||||
public @interface GeoSpatialIndexed {
|
||||
|
||||
/**
|
||||
* Index name. <br />
|
||||
* <br />
|
||||
* Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template
|
||||
* expression}. <br />
|
||||
* The name will only be applied as is when defined on root level. For usage on nested or embedded structures the
|
||||
* provided name will be prefixed with the path leading to the entity. <br />
|
||||
* <br />
|
||||
@@ -52,6 +52,7 @@ public @interface GeoSpatialIndexed {
|
||||
* @Document
|
||||
* class Hybrid {
|
||||
* @GeoSpatialIndexed(name="index") Point h1;
|
||||
* @GeoSpatialIndexed(name="#{@myBean.indexName}") Point h2;
|
||||
* }
|
||||
*
|
||||
* class Nested {
|
||||
@@ -67,6 +68,7 @@ public @interface GeoSpatialIndexed {
|
||||
* db.root.createIndex( { hybrid.h1: "2d" } , { name: "hybrid.index" } )
|
||||
* db.root.createIndex( { nested.n1: "2d" } , { name: "nested.index" } )
|
||||
* db.hybrid.createIndex( { h1: "2d" } , { name: "index" } )
|
||||
* db.hybrid.createIndex( { h2: "2d"} , { name: the value myBean.getIndexName() returned } )
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
|
||||
@@ -65,7 +65,8 @@ public @interface Indexed {
|
||||
boolean dropDups() default false;
|
||||
|
||||
/**
|
||||
* Index name. <br />
|
||||
* Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template
|
||||
* expression}. <br />
|
||||
* <br />
|
||||
* The name will only be applied as is when defined on root level. For usage on nested or embedded structures the
|
||||
* provided name will be prefixed with the path leading to the entity. <br />
|
||||
@@ -83,6 +84,7 @@ public @interface Indexed {
|
||||
* @Document
|
||||
* class Hybrid {
|
||||
* @Indexed(name="index") String h1;
|
||||
* @Indexed(name="#{@myBean.indexName}") String h2;
|
||||
* }
|
||||
*
|
||||
* class Nested {
|
||||
@@ -98,6 +100,7 @@ public @interface Indexed {
|
||||
* db.root.createIndex( { hybrid.h1: 1 } , { name: "hybrid.index" } )
|
||||
* db.root.createIndex( { nested.n1: 1 } , { name: "nested.index" } )
|
||||
* db.hybrid.createIndex( { h1: 1} , { name: "index" } )
|
||||
* db.hybrid.createIndex( { h2: 1} , { name: the value myBean.getIndexName() returned } )
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
@@ -135,7 +138,8 @@ public @interface Indexed {
|
||||
/**
|
||||
* Alternative for {@link #expireAfterSeconds()} to configure the timeout after which the collection should expire.
|
||||
* Defaults to an empty String for no expiry. Accepts numeric values followed by their unit of measure (d(ays),
|
||||
* h(ours), m(inutes), s(seconds)) or a Spring {@literal template expression}.
|
||||
* h(ours), m(inutes), s(seconds)) or a Spring {@literal template expression}. The expression can result in a a valid
|
||||
* expiration {@link String} following the conventions already mentioned or a {@link java.time.Duration}.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.AssociationHandler;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.CycleGuard.Path;
|
||||
@@ -62,6 +63,7 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.NumberUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -356,10 +358,10 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
MongoPersistentEntity<?> entity) {
|
||||
|
||||
CompoundIndexDefinition indexDefinition = new CompoundIndexDefinition(
|
||||
resolveCompoundIndexKeyFromStringDefinition(dotPath, index.def()));
|
||||
resolveCompoundIndexKeyFromStringDefinition(dotPath, index.def(), entity));
|
||||
|
||||
if (!index.useGeneratedName()) {
|
||||
indexDefinition.named(pathAwareIndexName(index.name(), dotPath, null));
|
||||
indexDefinition.named(pathAwareIndexName(index.name(), dotPath, entity, null));
|
||||
}
|
||||
|
||||
if (index.unique()) {
|
||||
@@ -377,7 +379,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
|
||||
}
|
||||
|
||||
private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString) {
|
||||
private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString,
|
||||
PersistentEntity<?, ?> entity) {
|
||||
|
||||
if (!StringUtils.hasText(dotPath) && !StringUtils.hasText(keyDefinitionString)) {
|
||||
throw new InvalidDataAccessApiUsageException("Cannot create index on root level for empty keys.");
|
||||
@@ -387,7 +390,12 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
return new org.bson.Document(dotPath, 1);
|
||||
}
|
||||
|
||||
org.bson.Document dbo = org.bson.Document.parse(keyDefinitionString);
|
||||
Object keyDefToUse = evaluatePotentialTemplateExpression(keyDefinitionString,
|
||||
getEvaluationContextForProperty(entity));
|
||||
|
||||
org.bson.Document dbo = (keyDefToUse instanceof org.bson.Document) ? (org.bson.Document) keyDefToUse
|
||||
: org.bson.Document.parse(ObjectUtils.nullSafeToString(keyDefToUse));
|
||||
|
||||
if (!StringUtils.hasText(dotPath)) {
|
||||
return dbo;
|
||||
}
|
||||
@@ -423,7 +431,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
IndexDirection.ASCENDING.equals(index.direction()) ? Sort.Direction.ASC : Sort.Direction.DESC);
|
||||
|
||||
if (!index.useGeneratedName()) {
|
||||
indexDefinition.named(pathAwareIndexName(index.name(), dotPath, persitentProperty));
|
||||
indexDefinition.named(pathAwareIndexName(index.name(), dotPath, persitentProperty.getOwner(), persitentProperty));
|
||||
}
|
||||
|
||||
if (index.unique()) {
|
||||
@@ -446,21 +454,12 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
|
||||
if (index.expireAfterSeconds() >= 0) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"@Indexed already defines an expiration timeout of %s sec. via Indexed#expireAfterSeconds. Please make to use either expireAfterSeconds or expireAfter.", index.expireAfterSeconds()));
|
||||
"@Indexed already defines an expiration timeout of %s sec. via Indexed#expireAfterSeconds. Please make to use either expireAfterSeconds or expireAfter.",
|
||||
index.expireAfterSeconds()));
|
||||
}
|
||||
|
||||
EvaluationContext ctx = getEvaluationContext();
|
||||
|
||||
if (persitentProperty.getOwner() instanceof BasicMongoPersistentEntity) {
|
||||
|
||||
EvaluationContext contextFromEntity = ((BasicMongoPersistentEntity<?>) persitentProperty.getOwner())
|
||||
.getEvaluationContext(null);
|
||||
if (contextFromEntity != null && !EvaluationContextProvider.DEFAULT.equals(contextFromEntity)) {
|
||||
ctx = contextFromEntity;
|
||||
}
|
||||
}
|
||||
|
||||
Duration timeout = computeIndexTimeout(index.expireAfter(), ctx);
|
||||
Duration timeout = computeIndexTimeout(index.expireAfter(),
|
||||
getEvaluationContextForProperty(persitentProperty.getOwner()));
|
||||
if (!timeout.isZero() && !timeout.isNegative()) {
|
||||
indexDefinition.expire(timeout);
|
||||
}
|
||||
@@ -479,6 +478,27 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
return evaluationContextProvider.getEvaluationContext(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link EvaluationContext} for a given {@link PersistentEntity entity} the default one.
|
||||
*
|
||||
* @param persistentEntity can be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
private EvaluationContext getEvaluationContextForProperty(@Nullable PersistentEntity<?, ?> persistentEntity) {
|
||||
|
||||
if (persistentEntity == null || !(persistentEntity instanceof BasicMongoPersistentEntity)) {
|
||||
return getEvaluationContext();
|
||||
}
|
||||
|
||||
EvaluationContext contextFromEntity = ((BasicMongoPersistentEntity<?>) persistentEntity).getEvaluationContext(null);
|
||||
|
||||
if (contextFromEntity != null && !EvaluationContextProvider.DEFAULT.equals(contextFromEntity)) {
|
||||
return contextFromEntity;
|
||||
}
|
||||
|
||||
return getEvaluationContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link EvaluationContextProvider} used for obtaining the {@link EvaluationContext} used to compute
|
||||
* {@link org.springframework.expression.spel.standard.SpelExpression expressions}.
|
||||
@@ -514,7 +534,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
indexDefinition.withMin(index.min()).withMax(index.max());
|
||||
|
||||
if (!index.useGeneratedName()) {
|
||||
indexDefinition.named(pathAwareIndexName(index.name(), dotPath, persistentProperty));
|
||||
indexDefinition
|
||||
.named(pathAwareIndexName(index.name(), dotPath, persistentProperty.getOwner(), persistentProperty));
|
||||
}
|
||||
|
||||
indexDefinition.typed(index.type()).withBucketSize(index.bucketSize()).withAdditionalField(index.additionalField());
|
||||
@@ -522,9 +543,13 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
|
||||
}
|
||||
|
||||
private String pathAwareIndexName(String indexName, String dotPath, @Nullable MongoPersistentProperty property) {
|
||||
private String pathAwareIndexName(String indexName, String dotPath, @Nullable PersistentEntity<?, ?> entity,
|
||||
@Nullable MongoPersistentProperty property) {
|
||||
|
||||
String nameToUse = StringUtils.hasText(indexName) ? indexName : "";
|
||||
String nameToUse = StringUtils.hasText(indexName)
|
||||
? ObjectUtils
|
||||
.nullSafeToString(evaluatePotentialTemplateExpression(indexName, getEvaluationContextForProperty(entity)))
|
||||
: "";
|
||||
|
||||
if (!StringUtils.hasText(dotPath) || (property != null && dotPath.equals(property.getFieldName()))) {
|
||||
return StringUtils.hasText(nameToUse) ? nameToUse : dotPath;
|
||||
@@ -582,7 +607,17 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
*/
|
||||
private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) {
|
||||
|
||||
String val = evaluatePotentialTemplateExpression(timeoutValue, evaluationContext);
|
||||
Object evaluatedTimeout = evaluatePotentialTemplateExpression(timeoutValue, evaluationContext);
|
||||
|
||||
if (evaluatedTimeout == null) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
if (evaluatedTimeout instanceof Duration) {
|
||||
return (Duration) evaluatedTimeout;
|
||||
}
|
||||
|
||||
String val = evaluatedTimeout.toString();
|
||||
|
||||
if (val == null) {
|
||||
return Duration.ZERO;
|
||||
@@ -611,15 +646,14 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String evaluatePotentialTemplateExpression(String value, EvaluationContext evaluationContext) {
|
||||
private static Object evaluatePotentialTemplateExpression(String value, EvaluationContext evaluationContext) {
|
||||
|
||||
Expression expression = PARSER.parseExpression(value, ParserContext.TEMPLATE_EXPRESSION);
|
||||
if (expression instanceof LiteralExpression) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return expression.getValue(evaluationContext, String.class);
|
||||
|
||||
return expression.getValue(evaluationContext, Object.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -220,6 +220,15 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 11L);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void shouldResolveTimeoutFromExpressionReturningDuration() {
|
||||
|
||||
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
|
||||
WithExpireAfterAsExpressionResultingInDuration.class);
|
||||
|
||||
Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 100L);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void shouldErrorOnInvalidTimeoutExpression() {
|
||||
|
||||
@@ -241,6 +250,15 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
.resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithDuplicateExpiry.class)));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void resolveExpressionIndexName() {
|
||||
|
||||
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
|
||||
WithIndexNameAsExpression.class);
|
||||
|
||||
Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st");
|
||||
}
|
||||
|
||||
@Document("Zero")
|
||||
static class IndexOnLevelZero {
|
||||
@Indexed String indexedProperty;
|
||||
@@ -341,6 +359,11 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
@Indexed(expireAfter = "#{10 + 1 + 's'}") String withTimeout;
|
||||
}
|
||||
|
||||
@Document
|
||||
static class WithExpireAfterAsExpressionResultingInDuration {
|
||||
@Indexed(expireAfter = "#{T(java.time.Duration).ofSeconds(100)}") String withTimeout;
|
||||
}
|
||||
|
||||
@Document
|
||||
class WithInvalidExpireAfter {
|
||||
@Indexed(expireAfter = "123ops") String withTimeout;
|
||||
@@ -350,6 +373,11 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
class WithDuplicateExpiry {
|
||||
@Indexed(expireAfter = "1s", expireAfterSeconds = 2) String withTimeout;
|
||||
}
|
||||
|
||||
@Document
|
||||
static class WithIndexNameAsExpression {
|
||||
@Indexed(name = "#{'my' + 1 + 'st'}") String spelIndexName;
|
||||
}
|
||||
}
|
||||
|
||||
@Target({ ElementType.FIELD })
|
||||
@@ -427,6 +455,15 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
isBsonObject().containing("name", "my_geo_index_name").containing("bucketSize", 2.0));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void resolveExpressionIndexNameForGeoIndex() {
|
||||
|
||||
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
|
||||
GeoIndexWithNameAsExpression.class);
|
||||
|
||||
Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st");
|
||||
}
|
||||
|
||||
@Document("Zero")
|
||||
static class GeoSpatialIndexOnLevelZero {
|
||||
@GeoSpatialIndexed Point geoIndexedProperty;
|
||||
@@ -474,6 +511,11 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
GeoSpatialIndexType indexType() default GeoSpatialIndexType.GEO_HAYSTACK;
|
||||
}
|
||||
|
||||
@Document
|
||||
static class GeoIndexWithNameAsExpression {
|
||||
@GeoSpatialIndexed(name = "#{'my' + 1 + 'st'}") Point spelIndexName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -573,6 +615,26 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
.containing("unique", true).containing("background", true));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void resolveExpressionIndexNameForCompoundIndex() {
|
||||
|
||||
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
|
||||
CompoundIndexWithNameExpression.class);
|
||||
|
||||
Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "cmp2name");
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void resolveExpressionDefForCompoundIndex() {
|
||||
|
||||
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
|
||||
CompoundIndexWithDefExpression.class);
|
||||
|
||||
assertThat(indexDefinitions, hasSize(1));
|
||||
assertIndexPathAndCollection(new String[] { "foo", "bar" }, "compoundIndexWithDefExpression",
|
||||
indexDefinitions.get(0));
|
||||
}
|
||||
|
||||
@Document("CompoundIndexOnLevelOne")
|
||||
static class CompoundIndexOnLevelOne {
|
||||
|
||||
@@ -637,6 +699,14 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
|
||||
}
|
||||
|
||||
@Document
|
||||
@CompoundIndex(name = "#{'cmp' + 2 + 'name'}", def = "{'foo': 1, 'bar': -1}")
|
||||
static class CompoundIndexWithNameExpression {}
|
||||
|
||||
@Document
|
||||
@CompoundIndex(def = "#{T(org.bson.Document).parse(\"{ 'foo': 1, 'bar': -1 }\")}")
|
||||
static class CompoundIndexWithDefExpression {}
|
||||
|
||||
}
|
||||
|
||||
public static class TextIndexedResolutionTests {
|
||||
|
||||
Reference in New Issue
Block a user