diff --git a/pom.xml b/pom.xml index a6d5da9170..82469340bb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3225-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 0033bd11d5..f94e7738f1 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3225-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index f62c8dc7f4..dac15bccbb 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3225-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index c1efaea420..8f7357ec35 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3225-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index e7fae4df5c..4d5349f7e7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -115,6 +115,10 @@ private static Converter getIndexDefinitionIndexO ops = ops.collation(fromDocument(indexOptions.get("collation", Document.class))); } + if(indexOptions.containsKey("wildcardProjection")) { + ops.wildcardProjection(indexOptions.get("wildcardProjection", Document.class)); + } + return ops; }; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java index d0a1da68ea..7883da2270 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java @@ -29,7 +29,7 @@ public final class IndexField { enum Type { - GEO, TEXT, DEFAULT, HASH; + GEO, TEXT, DEFAULT, HASH, WILDCARD; } private final String key; @@ -48,7 +48,7 @@ private IndexField(String key, @Nullable Direction direction, @Nullable Type typ if (Type.GEO.equals(type) || Type.TEXT.equals(type)) { Assert.isNull(direction, "Geo/Text indexes must not have a direction!"); } else { - if (!Type.HASH.equals(type)) { + if (!(Type.HASH.equals(type) || Type.WILDCARD.equals(type))) { Assert.notNull(direction, "Default indexes require a direction"); } } @@ -77,6 +77,17 @@ static IndexField hashed(String key) { return new IndexField(key, null, Type.HASH); } + /** + * Creates a {@literal wildcard} {@link IndexField} for the given key. + * + * @param key must not be {@literal null} or empty. + * @return new instance of {@link IndexField}. + * @since 3.3 + */ + static IndexField wildcard(String key) { + return new IndexField(key, null, Type.WILDCARD); + } + /** * Creates a geo {@link IndexField} for the given key. * @@ -142,6 +153,16 @@ public boolean isHashed() { return Type.HASH.equals(type); } + /** + * Returns whether the {@link IndexField} is contains a {@literal wildcard} expression. + * + * @return {@literal true} if {@link IndexField} contains a wildcard {@literal $**}. + * @since 3.3 + */ + public boolean isWildcard() { + return Type.WILDCARD.equals(type); + } + /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java index 7b507a8727..f8370b1bc6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java @@ -55,6 +55,7 @@ public class IndexInfo { private @Nullable Duration expireAfter; private @Nullable String partialFilterExpression; private @Nullable Document collation; + private @Nullable Document wildcardProjection; public IndexInfo(List indexFields, String name, boolean unique, boolean sparse, String language) { @@ -99,6 +100,8 @@ public static IndexInfo indexInfoOf(Document sourceDocument) { if (ObjectUtils.nullSafeEquals("hashed", value)) { indexFields.add(IndexField.hashed(key)); + } else if (key.contains("$**")) { + indexFields.add(IndexField.wildcard(key)); } else { Double keyValue = new Double(value.toString()); @@ -131,6 +134,10 @@ public static IndexInfo indexInfoOf(Document sourceDocument) { info.expireAfter = Duration.ofSeconds(NumberUtils.convertNumberToTargetClass(expireAfterSeconds, Long.class)); } + if (sourceDocument.containsKey("wildcardProjection")) { + info.wildcardProjection = sourceDocument.get("wildcardProjection", Document.class); + } + return info; } @@ -216,6 +223,16 @@ public Optional getCollation() { return Optional.ofNullable(collation); } + /** + * Get {@literal wildcardProjection} information. + * + * @return {@link Optional#empty() empty} if not set. + * @since 3.3 + */ + public Optional getWildcardProjection() { + return Optional.ofNullable(wildcardProjection); + } + /** * Get the duration after which documents within the index expire. * @@ -234,6 +251,14 @@ public boolean isHashed() { return getIndexFields().stream().anyMatch(IndexField::isHashed); } + /** + * @return {@literal true} if a wildcard index field is present. + * @since 3.3 + */ + public boolean isWildcard() { + return getIndexFields().stream().anyMatch(IndexField::isWildcard); + } + @Override public String toString() { @@ -303,4 +328,5 @@ public boolean equals(Object obj) { } return true; } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index 2fc63fb36c..78f895e077 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -46,6 +46,7 @@ import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.DotPath; import org.springframework.data.spel.EvaluationContextProvider; @@ -121,6 +122,7 @@ public List resolveIndexForEntity(MongoPersistentEntity indexInformation = new ArrayList<>(); String collection = root.getCollection(); indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", collection, root)); + indexInformation.addAll(potentiallyCreateWildcardIndexDefinitions("", collection, root)); indexInformation.addAll(potentiallyCreateTextIndexDefinition(root, collection)); root.doWithProperties((PropertyHandler) property -> this @@ -162,17 +164,18 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo * @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property * types. Will never be {@code null}. */ - private List resolveIndexForClass( TypeInformation type, String dotPath, - Path path, String collection, CycleGuard guard) { + private List resolveIndexForClass(TypeInformation type, String dotPath, Path path, + String collection, CycleGuard guard) { return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type), dotPath, path, collection, guard); } - private List resolveIndexForEntity(MongoPersistentEntity entity, String dotPath, - Path path, String collection, CycleGuard guard) { + private List resolveIndexForEntity(MongoPersistentEntity entity, String dotPath, Path path, + String collection, CycleGuard guard) { List indexInformation = new ArrayList<>(); indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(dotPath, collection, entity)); + indexInformation.addAll(potentiallyCreateWildcardIndexDefinitions(dotPath, collection, entity)); entity.doWithProperties((PropertyHandler) property -> this .guardAndPotentiallyAddIndexForProperty(property, dotPath, path, collection, indexInformation, guard)); @@ -196,15 +199,15 @@ private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty pers if (persistentProperty.isEntity()) { try { - indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), propertyDotPath.toString(), - propertyPath, collection, guard)); + indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), + propertyDotPath.toString(), propertyPath, collection, guard)); } catch (CyclicPropertyReferenceException e) { LOGGER.info(e.getMessage()); } } - List indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), collection, - persistentProperty); + List indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), + collection, persistentProperty); if (!indexDefinitions.isEmpty()) { indexes.addAll(indexDefinitions); @@ -232,6 +235,11 @@ private List createIndexDefinitionHolderForProperty(Strin if (persistentProperty.isAnnotationPresent(HashIndexed.class)) { indices.add(createHashedIndexDefinition(dotPath, collection, persistentProperty)); } + if (persistentProperty.isAnnotationPresent(WildcardIndexed.class)) { + indices.add(createWildcardIndexDefinition(dotPath, collection, + persistentProperty.getRequiredAnnotation(WildcardIndexed.class), + mappingContext.getPersistentEntity(persistentProperty))); + } return indices; } @@ -246,6 +254,18 @@ private List potentiallyCreateCompoundIndexDefinitions(St return createCompoundIndexDefinitions(dotPath, collection, entity); } + private List potentiallyCreateWildcardIndexDefinitions(String dotPath, String collection, + MongoPersistentEntity entity) { + + if (entity.findAnnotation(WildcardIndexed.class) == null) { + return Collections.emptyList(); + } + + return Collections.singletonList(new IndexDefinitionHolder(dotPath, + createWildcardIndexDefinition(dotPath, collection, entity.getRequiredAnnotation(WildcardIndexed.class), entity), + collection)); + } + private Collection potentiallyCreateTextIndexDefinition( MongoPersistentEntity root, String collection) { @@ -292,9 +312,8 @@ private Collection potentiallyCreateTextIndexDe } - private void appendTextIndexInformation(DotPath dotPath, Path path, - TextIndexDefinitionBuilder indexDefinitionBuilder, MongoPersistentEntity entity, - TextIndexIncludeOptions includeOptions, CycleGuard guard) { + private void appendTextIndexInformation(DotPath dotPath, Path path, TextIndexDefinitionBuilder indexDefinitionBuilder, + MongoPersistentEntity entity, TextIndexIncludeOptions includeOptions, CycleGuard guard) { entity.doWithProperties(new PropertyHandler() { @@ -311,8 +330,7 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) if (includeOptions.isForce() || indexed != null || persistentProperty.isEntity()) { - DotPath propertyDotPath = dotPath - .append(persistentProperty.getFieldName()); + DotPath propertyDotPath = dotPath.append(persistentProperty.getFieldName()); Path propertyPath = path.append(persistentProperty); @@ -406,6 +424,32 @@ protected IndexDefinitionHolder createCompoundIndexDefinition(String dotPath, St return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } + protected IndexDefinitionHolder createWildcardIndexDefinition(String dotPath, String collection, + WildcardIndexed index, @Nullable MongoPersistentEntity entity) { + + WildcardIndex indexDefinition = new WildcardIndex(dotPath); + + if (StringUtils.hasText(index.wildcardProjection())) { + indexDefinition.wildcardProjection(evaluateWildcardProjection(index.wildcardProjection(), entity)); + } + + if (!index.useGeneratedName()) { + indexDefinition.named(pathAwareIndexName(index.name(), dotPath, entity, null)); + } + + if (StringUtils.hasText(index.partialFilter())) { + indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity)); + } + + if (StringUtils.hasText(index.collation())) { + indexDefinition.collation(evaluateCollation(index.collation(), entity)); + } else if (entity != null && entity.hasCollation()) { + indexDefinition.collation(entity.getCollation()); + } + + return new IndexDefinitionHolder(dotPath, indexDefinition, collection); + } + private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString, PersistentEntity entity) { @@ -510,6 +554,33 @@ private PartialIndexFilter evaluatePartialFilter(String filterExpression, Persis return PartialIndexFilter.of(BsonUtils.parse(filterExpression, null)); } + private org.bson.Document evaluateWildcardProjection(String projectionExpression, PersistentEntity entity) { + + Object result = evaluate(projectionExpression, getEvaluationContextForProperty(entity)); + + if (result instanceof org.bson.Document) { + return (org.bson.Document) result; + } + + return BsonUtils.parse(projectionExpression, null); + } + + private Collation evaluateCollation(String collationExpression, PersistentEntity entity) { + + Object result = evaluate(collationExpression, getEvaluationContextForProperty(entity)); + if (result instanceof org.bson.Document) { + return Collation.from((org.bson.Document) result); + } + if (result instanceof Collation) { + return (Collation) result; + } + if (result instanceof String) { + return Collation.parse(result.toString()); + } + throw new IllegalStateException("Cannot parse collation " + result); + + } + /** * Creates {@link HashedIndex} wrapped in {@link IndexDefinitionHolder} out of {@link HashIndexed} for a given * {@link MongoPersistentProperty}. @@ -657,8 +728,8 @@ private void resolveAndAddIndexesForAssociation(Association indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), collection, - property); + List indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), + collection, property); if (!indexDefinitions.isEmpty()) { indexes.addAll(indexDefinitions); @@ -998,6 +1069,11 @@ public org.bson.Document getIndexKeys() { public org.bson.Document getIndexOptions() { return indexDefinition.getIndexOptions(); } + + @Override + public String toString() { + return "IndexDefinitionHolder{" + "indexKeys=" + getIndexKeys() + '}'; + } } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java new file mode 100644 index 0000000000..ab1cda6183 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java @@ -0,0 +1,198 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.index; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.bson.Document; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link WildcardIndex} is a specific {@link Index} that can be used to include all fields into an index based on the + * {@code $**" : 1} pattern on a root object (the one typically carrying the + * {@link org.springframework.data.mongodb.core.mapping.Document} annotation). On those it is possible to use + * {@link #wildcardProjectionInclude(String...)} and {@link #wildcardProjectionExclude(String...)} to define specific + * paths for in-/exclusion. + *

+ * It can also be used to define an index on a specific field path and its subfields, e.g. + * {@code "path.to.field.$**" : 1}.
+ * Note that {@literal wildcardProjections} are not allowed in this case. + *

+ * LIMITATIONS
+ *

    + *
  • {@link #unique() Unique} and {@link #expire(long) ttl} options are not supported.
  • + *
  • Keys used for sharding must not be included
  • + *
  • Cannot be used to generate any type of geo index.
  • + *
+ * + * @author Christoph Strobl + * @see MongoDB Reference Documentation: Wildcard + * Indexes/ + * @since 3.3 + */ +public class WildcardIndex extends Index { + + private @Nullable String fieldName; + private Map wildcardProjection = new LinkedHashMap<>(); + + /** + * Create a new instance of {@link WildcardIndex} using {@code $**}. + */ + public WildcardIndex() {} + + /** + * Create a new instance of {@link WildcardIndex} for the given {@literal path}. If no {@literal path} is provided the + * index will be considered a root one using {@code $**}.
+ * NOTE {@link #wildcardProjectionInclude(String...)}, {@link #wildcardProjectionExclude(String...)} + * can only be used for top level index definitions having an {@literal empty} or {@literal null} path. + * + * @param path can be {@literal null}. If {@literal null} all fields will be indexed. + */ + public WildcardIndex(@Nullable String path) { + this.fieldName = path; + } + + /** + * Include the {@code _id} field in {@literal wildcardProjection}. + * + * @return this. + */ + public WildcardIndex includeId() { + + wildcardProjection.put("_id", 1); + return this; + } + + /** + * Set the index name to use. + * + * @param name + * @return this. + */ + @Override + public WildcardIndex named(String name) { + + super.named(name); + return this; + } + + /** + * Unique option is not supported. + * + * @throws UnsupportedOperationException + */ + @Override + public Index unique() { + throw new UnsupportedOperationException("Wildcard Index does not support 'unique'."); + } + + /** + * ttl option is not supported. + * + * @throws UnsupportedOperationException + */ + @Override + public Index expire(long seconds) { + throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'."); + } + + /** + * ttl option is not supported. + * + * @throws UnsupportedOperationException + */ + @Override + public Index expire(long value, TimeUnit timeUnit) { + throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'."); + } + + /** + * ttl option is not supported. + * + * @throws UnsupportedOperationException + */ + @Override + public Index expire(Duration duration) { + throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'."); + } + + /** + * Add fields to be included from indexing via {@code wildcardProjection}.
+ * This option is only allowed on {@link WildcardIndex#WildcardIndex() top level} wildcard indexes. + * + * @param paths must not be {@literal null}. + * @return this. + */ + public WildcardIndex wildcardProjectionInclude(String... paths) { + + for (String path : paths) { + wildcardProjection.put(path, 1); + } + return this; + } + + /** + * Add fields to be excluded from indexing via {@code wildcardProjection}.
+ * This option is only allowed on {@link WildcardIndex#WildcardIndex() top level} wildcard indexes. + * + * @param paths must not be {@literal null}. + * @return this. + */ + public WildcardIndex wildcardProjectionExclude(String... paths) { + + for (String path : paths) { + wildcardProjection.put(path, 0); + } + return this; + } + + /** + * Set the fields to be in-/excluded from indexing via {@code wildcardProjection}.
+ * This option is only allowed on {@link WildcardIndex#WildcardIndex() top level} wildcard indexes. + * + * @param includeExclude must not be {@literal null}. + * @return this. + */ + public WildcardIndex wildcardProjection(Map includeExclude) { + + wildcardProjection.putAll(includeExclude); + return this; + } + + private String getTargetFieldName() { + return StringUtils.hasText(fieldName) ? (fieldName + ".$**") : "$**"; + } + + @Override + public Document getIndexKeys() { + return new Document(getTargetFieldName(), 1); + } + + @Override + public Document getIndexOptions() { + + Document options = new Document(super.getIndexOptions()); + if (!CollectionUtils.isEmpty(wildcardProjection)) { + options.put("wildcardProjection", new Document(wildcardProjection)); + } + return options; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java new file mode 100644 index 0000000000..5f32aaf45c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java @@ -0,0 +1,130 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for an entity or property that should be used as key for a + * Wildcard Index.
+ * If placed on a {@link ElementType#TYPE type} that is a root level domain entity (one having an + * {@link org.springframework.data.mongodb.core.mapping.Document} annotation) will advise the index creator to create a + * wildcard index for it. + * + *
+ *
+ * @Document
+ * @WildcardIndexed
+ * public class Product {
+ *     ...
+ * }
+ *
+ * db.product.createIndex({ "$**" : 1 } , {})
+ * 
+ * + * {@literal wildcardProjection} can be used to specify keys to in-/exclude in the index. + * + *
+ *
+ * @Document
+ * @WildcardIndexed(wildcardProjection = "{ 'userMetadata.age' : 0 }")
+ * public class User {
+ *     private @Id String id;
+ *     private UserMetadata userMetadata;
+ * }
+ *
+ *
+ * db.user.createIndex(
+ *   { "$**" : 1 },
+ *   { "wildcardProjection" :
+ *     { "userMetadata.age" : 0 }
+ *   }
+ * )
+ * 
+ * + * Wildcard indexes can also be expressed by adding the annotation directly to the field. Please note that + * {@literal wildcardProjection} is not allowed on nested paths. + * + *
+ * @Document
+ * public class User {
+ * 
+ *     private @Id String id;
+ *
+ *     @WildcardIndexed
+ *     private UserMetadata userMetadata;
+ * }
+ *
+ *
+ * db.user.createIndex({ "userMetadata.$**" : 1 }, {})
+ * 
+ * + * @author Christoph Strobl + * @since 3.3 + */ +@Documented +@Target({ ElementType.TYPE, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface WildcardIndexed { + + /** + * Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template + * expression}.
+ *
+ * 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.
+ * + * @return + */ + String name() default ""; + + /** + * If set to {@literal true} then MongoDB will ignore the given index name and instead generate a new name. Defaults + * to {@literal false}. + * + * @return {@literal false} by default. + */ + boolean useGeneratedName() default false; + + /** + * Only index the documents in a collection that meet a specified {@link IndexFilter filter expression}.
+ * + * @return empty by default. + * @see https://docs.mongodb.com/manual/core/index-partial/ + */ + String partialFilter() default ""; + + /** + * Explicitly specify sub fields to be in-/excluded as a {@link org.bson.Document#parse(String) prasable} String. + *
+ * NOTE: Can only be done on root level documents. + * + * @return empty by default. + */ + String wildcardProjection() default ""; + + /** + * Defines the collation to apply. + * + * @return an empty {@link String} by default. + */ + String collation() default ""; +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java index 2026dfc644..3618e4c1f9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java @@ -36,6 +36,7 @@ public class IndexInfoUnitTests { static final String INDEX_WITH_PARTIAL_FILTER = "{ \"v\" : 2, \"key\" : { \"k3y\" : 1 }, \"name\" : \"partial-filter-index\", \"ns\" : \"db.collection\", \"partialFilterExpression\" : { \"quantity\" : { \"$gte\" : 10 } } }"; static final String INDEX_WITH_EXPIRATION_TIME = "{ \"v\" : 2, \"key\" : { \"lastModifiedDate\" : 1 },\"name\" : \"expire-after-last-modified\", \"ns\" : \"db.collectio\", \"expireAfterSeconds\" : 3600 }"; static final String HASHED_INDEX = "{ \"v\" : 2, \"key\" : { \"score\" : \"hashed\" }, \"name\" : \"score_hashed\", \"ns\" : \"db.collection\" }"; + static final String WILDCARD_INDEX = "{ \"v\" : 2, \"key\" : { \"$**\" : 1 }, \"name\" : \"$**_1\", \"wildcardProjection\" : { \"fieldA\" : 0, \"fieldB.fieldC\" : 0 } }"; @Test public void isIndexForFieldsCorrectly() { @@ -79,6 +80,16 @@ public void hashedIndexIsMarkedAsSuch() { assertThat(getIndexInfo(HASHED_INDEX).isHashed()).isTrue(); } + @Test // GH-3225 + public void identifiesWildcardIndexCorrectly() { + assertThat(getIndexInfo(WILDCARD_INDEX).isWildcard()).isTrue(); + } + + @Test // GH-3225 + public void readsWildcardIndexProjectionCorrectly() { + assertThat(getIndexInfo(WILDCARD_INDEX).getWildcardProjection()).contains(new Document("fieldA", 0).append("fieldB.fieldC", 0)); + } + private static IndexInfo getIndexInfo(String documentJson) { return IndexInfo.indexInfoOf(Document.parse(documentJson)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index 489070548d..0a06561b67 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -15,8 +15,9 @@ */ package org.springframework.data.mongodb.core.index; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; -import static org.springframework.data.mongodb.test.util.Assertions.*; +import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -25,6 +26,7 @@ import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; @@ -1323,6 +1325,49 @@ public void errorsOnIndexOnEmbedded() { } + @Test // GH-3225 + public void resolvesWildcardOnRoot() { + + List indices = prepareMappingContextAndResolveIndexForType( + WithWildCardIndexOnEntity.class); + assertThat(indices).hasSize(1); + assertThat(indices.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("$**", 1); + }); + } + + @Test // GH-3225 + public void resolvesWildcardOnProperty() { + + List indices = prepareMappingContextAndResolveIndexForType( + WithWildCardIndexOnProperty.class); + assertThat(indices).hasSize(3); + assertThat(indices.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("value.$**", 1); + }); + assertThat(indices.get(1)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("the_field.$**", 1); + }); + assertThat(indices.get(2)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("withOptions.$**", 1); + assertThat(it.getIndexOptions()).containsEntry("name", + "withOptions.idx") + .containsEntry("collation", new org.bson.Document("locale", "en_US")) + .containsEntry("partialFilterExpression", new org.bson.Document("$eq", 1)); + }); + } + + @Test // GH-3225 + public void resolvesWildcardTypeOfNestedProperty() { + + List indices = prepareMappingContextAndResolveIndexForType( + WithWildCardOnEntityOfNested.class); + assertThat(indices).hasSize(1); + assertThat(indices.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("value.$**", 1); + }); + } + @Document class MixedIndexRoot { @@ -1533,7 +1578,7 @@ class InvalidIndexOnUnwrapped { @Indexed // @Unwrapped.Nullable // - UnwrappableType unwrappableType; + UnwrappableType unwrappableType; } @@ -1573,6 +1618,42 @@ class WithHashedIndex { @HashIndexed String value; } + @Document + @WildcardIndexed + class WithWildCardIndexOnEntity { + + String value; + } + + @Document + @WildcardIndexed(wildcardProjection = "{'_id' : 1, 'value' : 0}") + class WithWildCardIndexHavingProjectionOnEntity { + + String value; + } + + @Document + class WithWildCardIndexOnProperty { + + @WildcardIndexed // + Map value; + + @WildcardIndexed // + @Field("the_field") // + Map renamedField; + + @WildcardIndexed(name = "idx", partialFilter = "{ '$eq' : 1 }", collation = "en_US") // + Map withOptions; + + } + + @Document + class WithWildCardOnEntityOfNested { + + WithWildCardIndexOnEntity value; + + } + @Document class WithHashedIndexAndIndex { diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index f08d03d3f0..7caf1093b9 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -760,6 +760,94 @@ mongoOperations.indexOpsFor(Jedi.class) ---- ==== +[[mapping-usage-indexes.wildcard-index]] +=== Wildcard Indexes + +A `WildcardIndex` is an index that can be used to include all fields or specific ones based a given (wildcard) pattern. +For details, refer to the https://docs.mongodb.com/manual/core/index-wildcard/[MongoDB Documentation]. + +The index can be set up programmatically using `WildcardIndex` via `IndexOperations`. + +.Programmatic WildcardIndex setup +==== +[source,java] +---- +mongoOperations + .indexOps(User.class) + .ensureIndex(new WildcardIndex("userMetadata")); +---- +[source,javascript] +---- +db.user.createIndex({ "userMetadata.$**" : 1 }, {}) +---- +==== + +The `@WildcardIndex` annotation allows a declarative index setup an can be added on either a type or property. + +If placed on a type that is a root level domain entity (one having an `@Document` annotation) will advise the index creator to create a +wildcard index for it. + +.Wildcard index on domain type +==== +[source,java] +---- +@Document +@WildcardIndexed +public class Product { + ... +} +---- +[source,javascript] +---- +db.product.createIndex({ "$**" : 1 },{}) +---- +==== + +The `wildcardProjection` can be used to specify keys to in-/exclude in the index. + +.Wildcard index with `wildcardProjection` +==== +[source,java] +---- +@Document +@WildcardIndexed(wildcardProjection = "{ 'userMetadata.age' : 0 }") +public class User { + private @Id String id; + private UserMetadata userMetadata; +} +---- +[source,javascript] +---- +db.user.createIndex( + { "$**" : 1 }, + { "wildcardProjection" : + { "userMetadata.age" : 0 } + } +) +---- +==== + +Wildcard indexes can also be expressed by adding the annotation directly to the field. +Please note that `wildcardProjection` is not allowed on nested paths. + +.Wildcard index on property +==== +[source,java] +---- +@Document +public class User { + private @Id String id; + + @WildcardIndexed + private UserMetadata userMetadata; +} +---- +[source,javascript] +---- +db.user.createIndex({ "userMetadata.$**" : 1 }, {}) +---- +==== + [[mapping-usage-indexes.text-index]] === Text Indexes