Add support for Wildcard Index.

Add WildcardIndexed annotation and the programatic WildcardIndex.

Closes #3225
Original pull request: #3671.
This commit is contained in:
Christoph Strobl
2021-06-15 16:51:28 +02:00
committed by Mark Paluch
parent 986ea39f90
commit d57c5a9529
9 changed files with 654 additions and 19 deletions

View File

@@ -115,6 +115,10 @@ abstract class IndexConverters {
ops = ops.collation(fromDocument(indexOptions.get("collation", Document.class)));
}
if(indexOptions.containsKey("wildcardProjection")) {
ops.wildcardProjection(indexOptions.get("wildcardProjection", Document.class));
}
return ops;
};
}

View File

@@ -29,7 +29,7 @@ import org.springframework.util.ObjectUtils;
public final class IndexField {
enum Type {
GEO, TEXT, DEFAULT, HASH;
GEO, TEXT, DEFAULT, HASH, WILDCARD;
}
private final String key;
@@ -48,7 +48,7 @@ public final class IndexField {
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 @@ public final class IndexField {
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 final class IndexField {
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)

View File

@@ -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<IndexField> indexFields, String name, boolean unique, boolean sparse, String language) {
@@ -99,6 +100,8 @@ public class IndexInfo {
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 class IndexInfo {
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 class IndexInfo {
return Optional.ofNullable(collation);
}
/**
* Get {@literal wildcardProjection} information.
*
* @return {@link Optional#empty() empty} if not set.
* @since 3.3
*/
public Optional<Document> getWildcardProjection() {
return Optional.ofNullable(wildcardProjection);
}
/**
* Get the duration after which documents within the index expire.
*
@@ -234,6 +251,14 @@ public class IndexInfo {
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 class IndexInfo {
}
return true;
}
}

View File

@@ -46,6 +46,7 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
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 class MongoPersistentEntityIndexResolver implements IndexResolver {
List<IndexDefinitionHolder> 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<MongoPersistentProperty>) property -> this
@@ -162,17 +164,18 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
* @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property
* types. Will never be {@code null}.
*/
private List<IndexDefinitionHolder> resolveIndexForClass( TypeInformation<?> type, String dotPath,
Path path, String collection, CycleGuard guard) {
private List<IndexDefinitionHolder> resolveIndexForClass(TypeInformation<?> type, String dotPath, Path path,
String collection, CycleGuard guard) {
return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type), dotPath, path, collection, guard);
}
private List<IndexDefinitionHolder> resolveIndexForEntity(MongoPersistentEntity<?> entity, String dotPath,
Path path, String collection, CycleGuard guard) {
private List<IndexDefinitionHolder> resolveIndexForEntity(MongoPersistentEntity<?> entity, String dotPath, Path path,
String collection, CycleGuard guard) {
List<IndexDefinitionHolder> indexInformation = new ArrayList<>();
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(dotPath, collection, entity));
indexInformation.addAll(potentiallyCreateWildcardIndexDefinitions(dotPath, collection, entity));
entity.doWithProperties((PropertyHandler<MongoPersistentProperty>) property -> this
.guardAndPotentiallyAddIndexForProperty(property, dotPath, path, collection, indexInformation, guard));
@@ -196,15 +199,15 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
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<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), collection,
persistentProperty);
List<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(),
collection, persistentProperty);
if (!indexDefinitions.isEmpty()) {
indexes.addAll(indexDefinitions);
@@ -232,6 +235,11 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
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 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return createCompoundIndexDefinitions(dotPath, collection, entity);
}
private List<IndexDefinitionHolder> 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<? extends IndexDefinitionHolder> potentiallyCreateTextIndexDefinition(
MongoPersistentEntity<?> root, String collection) {
@@ -292,9 +312,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
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<MongoPersistentProperty>() {
@@ -311,8 +330,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
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 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
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 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
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 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
propertyDotPath));
}
List<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), collection,
property);
List<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(),
collection, property);
if (!indexDefinitions.isEmpty()) {
indexes.addAll(indexDefinitions);
@@ -998,6 +1069,11 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
public org.bson.Document getIndexOptions() {
return indexDefinition.getIndexOptions();
}
@Override
public String toString() {
return "IndexDefinitionHolder{" + "indexKeys=" + getIndexKeys() + '}';
}
}
/**

View File

@@ -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.
* <p />
* It can also be used to define an index on a specific field path and its subfields, e.g.
* {@code "path.to.field.$**" : 1}. <br />
* Note that {@literal wildcardProjections} are not allowed in this case.
* <p />
* <strong>LIMITATIONS</strong><br />
* <ul>
* <li>{@link #unique() Unique} and {@link #expire(long) ttl} options are not supported.</li>
* <li>Keys used for sharding must not be included</li>
* <li>Cannot be used to generate any type of geo index.</li>
* </ul>
*
* @author Christoph Strobl
* @see <a href= "https://docs.mongodb.com/manual/core/index-wildcard/">MongoDB Reference Documentation: Wildcard
* Indexes/</a>
* @since 3.3
*/
public class WildcardIndex extends Index {
private @Nullable String fieldName;
private Map<String, Object> 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 $**}. <br />
* <strong>NOTE</strong> {@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}. <br />
* 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}. <br />
* 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}. <br />
* 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<String, Object> 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;
}
}

View File

@@ -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
* <a href="https://docs.mongodb.com/manual/core/index-wildcard/">Wildcard Index</a>. <br />
* 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.
*
* <pre class="code">
*
* &#64;Document
* &#64;WildcardIndexed
* public class Product {
* ...
* }
*
* db.product.createIndex({ "$**" : 1 } , {})
* </pre>
*
* {@literal wildcardProjection} can be used to specify keys to in-/exclude in the index.
*
* <pre class="code">
*
* &#64;Document
* &#64;WildcardIndexed(wildcardProjection = "{ 'userMetadata.age' : 0 }")
* public class User {
* private &#64;Id String id;
* private UserMetadata userMetadata;
* }
*
*
* db.user.createIndex(
* { "$**" : 1 },
* { "wildcardProjection" :
* { "userMetadata.age" : 0 }
* }
* )
* </pre>
*
* 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.
*
* <pre class="code">
* &#64;Document
* public class User {
*
* private &#64;Id String id;
*
* &#64;WildcardIndexed
* private UserMetadata userMetadata;
* }
*
*
* db.user.createIndex({ "userMetadata.$**" : 1 }, {})
* </pre>
*
* @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}. <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 />
*
* @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}. <br />
*
* @return empty by default.
* @see <a href=
* "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/</a>
*/
String partialFilter() default "";
/**
* Explicitly specify sub fields to be in-/excluded as a {@link org.bson.Document#parse(String) prasable} String.
* <br />
* <strong>NOTE: </strong>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 "";
}

View File

@@ -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 class IndexInfoUnitTests {
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));
}

View File

@@ -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.lang.annotation.Target;
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 class MongoPersistentEntityIndexResolverUnitTests {
}
@Test // GH-3225
public void resolvesWildcardOnRoot() {
List<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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 @@ public class MongoPersistentEntityIndexResolverUnitTests {
@Indexed //
@Unwrapped.Nullable //
UnwrappableType unwrappableType;
UnwrappableType unwrappableType;
}
@@ -1573,6 +1618,42 @@ public class MongoPersistentEntityIndexResolverUnitTests {
@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<String, String> value;
@WildcardIndexed //
@Field("the_field") //
Map<String, String> renamedField;
@WildcardIndexed(name = "idx", partialFilter = "{ '$eq' : 1 }", collation = "en_US") //
Map<String, String> withOptions;
}
@Document
class WithWildCardOnEntityOfNested {
WithWildCardIndexOnEntity value;
}
@Document
class WithHashedIndexAndIndex {

View File

@@ -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