Add support for Wildcard Index.
Add WildcardIndexed annotation and the programatic WildcardIndex. Closes #3225 Original pull request: #3671.
This commit is contained in:
committed by
Mark Paluch
parent
986ea39f90
commit
d57c5a9529
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() + '}';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
*
|
||||
* @Document
|
||||
* @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">
|
||||
*
|
||||
* @Document
|
||||
* @WildcardIndexed(wildcardProjection = "{ 'userMetadata.age' : 0 }")
|
||||
* public class User {
|
||||
* private @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">
|
||||
* @Document
|
||||
* public class User {
|
||||
*
|
||||
* private @Id String id;
|
||||
*
|
||||
* @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 "";
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user