Browse Source

Add support for Compound Wildcard Indexes.

See #4471
issue/4471
Julia 2 years ago committed by Marcin Grzejszczak
parent
commit
332e63ab8c
No known key found for this signature in database
GPG Key ID: 9663E23C6E20556A
  1. 60
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java
  2. 131
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java
  3. 61
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java
  4. 154
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java

60
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
/*
* Copyright 2014-2023 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 org.bson.Document;
import org.springframework.util.Assert;
/**
* {@link CompoundWildcardIndexDefinition} is a specific {@link Index} that includes one {@link WildcardIndex} and
* one or more non-wildcard fields.
*
* @author Julia Lee
* @since 4.2
*/
public class CompoundWildcardIndexDefinition extends WildcardIndex {
private final Document indexKeys;
/**
* Creates a new {@link CompoundWildcardIndexDefinition} for the given {@literal wildcardPath} and {@literal keys}.
* If {@literal wildcardPath} is empty, the wildcard index will apply to the root entity, using {@code $**}.
* <br />
*
* @param wildcardPath can be a {@literal empty} {@link String}.
*/
public CompoundWildcardIndexDefinition(String wildcardPath, Document indexKeys) {
super(wildcardPath);
this.indexKeys = indexKeys;
}
@Override
public Document getIndexKeys() {
Document document = new Document();
document.putAll(indexKeys);
document.putAll(super.getIndexKeys());
return document;
}
@Override
public Document getIndexOptions() {
Document options = super.getIndexOptions();
return options;
}
}

131
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
/*
* Copyright 2011-2023 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 org.springframework.core.annotation.AliasFor;
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;
/**
* Mark a class to use compound wildcard indexes. <br />
*
* <pre class="code">
* &#64;Document
* &#64;CompoundWildcardIndexed(wildcardFieldName = "address", fields = "{'firstname': 1}")
* class Person {
* String firstname;
* Address address;
* }
*
* db.product.createIndex({"address.$**": 1, "firstname": 1})
* </pre>
*
* {@literal wildcardProjection} can be used to specify keys to in-/exclude in the index.
*
* <pre class="code">
*
* &#64;Document
* &#64;CompoundWildcardIndexed(wildcardProjection = "{'address.zip': 0}", fields = "{'firstname': 1}")
* class Person {
* String firstname;
* Address address;
* }
*
* db.user.createIndex({"$**": 1, "firstname": 1}, {"wildcardProjection": {"address.zip": 0}})
* </pre>
*
* @author Julia Lee
*/
@Target({ ElementType.TYPE })
@Documented
@WildcardIndexed
@CompoundIndex
@Retention(RetentionPolicy.RUNTIME)
public @interface CompoundWildcardIndexed {
/**
* The name of the sub-field to which a wildcard index is applied. If empty, the wildcard term will resolve to "$**".
*
* @return empty by default.
*/
String wildcardFieldName() default "";
/**
* Explicitly specify sub-fields to be in-/excluded as a {@link org.bson.Document#parse(String) parsable} String.
* <br />
* <strong>NOTE:</strong> Can only be applied on when wildcard term is "$**"
*
* @return empty by default.
*/
@AliasFor(annotation = WildcardIndexed.class, attribute = "wildcardProjection")
String wildcardProjection() default "";
/**
* Definition of non-wildcard index(es) in JSON format, wherein the keys are the fields to be indexed and the values
* define the index direction (1 for ascending, -1 for descending). <br />
*
* <pre class="code">
* &#64;Document
* &#64;CompoundWildcardIndexed(wildcardProjection = "{ 'address.zip' : 0 }", fields = "{'firstname': 1}")
* class Person {
* String firstname;
* Address address;
* }
* </pre>
*
* @return empty String by default.
*/
@AliasFor(annotation = CompoundIndex.class, attribute = "def")
String fields();
/**
* Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template
* expression}. <br />
*
* @return empty by default.
*/
@AliasFor(annotation = WildcardIndexed.class, attribute = "name")
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
*/
@AliasFor(annotation = WildcardIndexed.class, attribute = "useGeneratedName")
boolean useGeneratedName() default false;
/**
* Only index the documents in a collection that meet a specified {@link IndexFilter filter expression}. <br />
*
* @return empty by default.
*/
@AliasFor(annotation = WildcardIndexed.class, attribute = "partialFilter")
String partialFilter() default "";
/**
* Defines the collation to apply.
*
* @return an empty {@link String} by default.
*/
@AliasFor(annotation = WildcardIndexed.class, attribute = "collation")
String collation() default "";
}

61
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java

@ -79,6 +79,7 @@ import org.springframework.util.StringUtils; @@ -79,6 +79,7 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch
* @author Dave Perryman
* @author Stefan Tirea
* @author Julia Lee
* @since 1.5
*/
public class MongoPersistentEntityIndexResolver implements IndexResolver {
@ -129,6 +130,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { @@ -129,6 +130,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", collection, root));
indexInformation.addAll(potentiallyCreateWildcardIndexDefinitions("", collection, root));
indexInformation.addAll(potentiallyCreateTextIndexDefinition(root, collection));
indexInformation.addAll(potentiallyCreateCompoundWildcardDefinition(root, collection));
root.doWithProperties((PropertyHandler<MongoPersistentProperty>) property -> this
.potentiallyAddIndexForProperty(root, property, indexInformation, new CycleGuard()));
@ -154,6 +156,22 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { @@ -154,6 +156,22 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
}
});
if (entity.isAnnotationPresent(CompoundWildcardIndexed.class)) {
CompoundWildcardIndexed indexed = entity.getRequiredAnnotation(CompoundWildcardIndexed.class);
if (!ObjectUtils.isEmpty(indexed.wildcardFieldName()) && !ObjectUtils.isEmpty(indexed.wildcardProjection())) {
throw new MappingException(
String.format("CompoundWildcardIndex.wildcardProjection is only allowed on \"$**\"; Offending property: %s",
indexed.wildcardFieldName()));
}
if (ObjectUtils.isEmpty(indexed.wildcardFieldName()) && ObjectUtils.isEmpty(indexed.wildcardProjection())) {
throw new MappingException(String.format("CompoundWildcardIndex.wildcardProjection is required on \"$**\""));
}
}
}
private void potentiallyAddIndexForProperty(MongoPersistentEntity<?> root, MongoPersistentProperty persistentProperty,
@ -280,7 +298,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { @@ -280,7 +298,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
private List<IndexDefinitionHolder> potentiallyCreateCompoundIndexDefinitions(String dotPath, String collection,
MongoPersistentEntity<?> entity) {
if (entity.findAnnotation(CompoundIndexes.class) == null && entity.findAnnotation(CompoundIndex.class) == null) {
if ((!entity.isAnnotationPresent(CompoundIndexes.class) && !entity.isAnnotationPresent(CompoundIndex.class))
|| entity.isAnnotationPresent(CompoundWildcardIndexed.class)) {
return Collections.emptyList();
}
@ -290,7 +309,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { @@ -290,7 +309,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
private List<IndexDefinitionHolder> potentiallyCreateWildcardIndexDefinitions(String dotPath, String collection,
MongoPersistentEntity<?> entity) {
if (!entity.isAnnotationPresent(WildcardIndexed.class)) {
if (!entity.isAnnotationPresent(WildcardIndexed.class)
|| entity.isAnnotationPresent(CompoundWildcardIndexed.class)) {
return Collections.emptyList();
}
@ -345,6 +365,19 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { @@ -345,6 +365,19 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
private Collection<? extends IndexDefinitionHolder> potentiallyCreateCompoundWildcardDefinition(
MongoPersistentEntity<?> entity, String collection) {
if (!entity.isAnnotationPresent(CompoundWildcardIndexed.class)) {
return Collections.emptyList();
}
CompoundWildcardIndexed compoundWildcardIndex = entity.getRequiredAnnotation(CompoundWildcardIndexed.class);
IndexDefinitionHolder compoundWildcardIndexDefinition = createCompoundWildcardIndexDefinition(collection,
compoundWildcardIndex, entity);
return Collections.singletonList(compoundWildcardIndexDefinition);
}
private void appendTextIndexInformation(DotPath dotPath, Path path, TextIndexDefinitionBuilder indexDefinitionBuilder,
MongoPersistentEntity<?> entity, TextIndexIncludeOptions includeOptions, CycleGuard guard) {
@ -483,6 +516,30 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { @@ -483,6 +516,30 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
}
protected IndexDefinitionHolder createCompoundWildcardIndexDefinition(String collection, CompoundWildcardIndexed index,
@Nullable MongoPersistentEntity<?> entity) {
String wildcardField = index.wildcardFieldName();
org.bson.Document indexKeys = resolveCompoundIndexKeyFromStringDefinition("", index.fields(), entity);
CompoundWildcardIndexDefinition indexDefinition = new CompoundWildcardIndexDefinition(wildcardField, indexKeys);
if (StringUtils.hasText(index.wildcardProjection()) && ObjectUtils.isEmpty(wildcardField)) {
indexDefinition.wildcardProjection(evaluateWildcardProjection(index.wildcardProjection(), entity));
}
if (StringUtils.hasText(index.partialFilter())) {
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity));
}
if (!index.useGeneratedName()) {
indexDefinition.named(pathAwareIndexName(index.name(), "", entity, null));
}
indexDefinition.collation(resolveCollation(index, entity));
return new IndexDefinitionHolder("", indexDefinition, collection);
}
private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString,
PersistentEntity<?, ?> entity) {

154
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java

@ -40,6 +40,7 @@ import org.springframework.data.mapping.MappingException; @@ -40,6 +40,7 @@ import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.CompoundIndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.CompoundWildcardIndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.GeoSpatialIndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.IndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.MixedIndexResolutionTests;
@ -62,10 +63,11 @@ import org.springframework.data.util.ClassTypeInformation; @@ -62,10 +63,11 @@ import org.springframework.data.util.ClassTypeInformation;
* @author Mark Paluch
* @author Dave Perryman
* @author Stefan Tirea
* @author Julia Lee
*/
@RunWith(Suite.class)
@SuiteClasses({ IndexResolutionTests.class, GeoSpatialIndexResolutionTests.class, CompoundIndexResolutionTests.class,
TextIndexedResolutionTests.class, MixedIndexResolutionTests.class })
CompoundWildcardIndexResolutionTests.class, TextIndexedResolutionTests.class, MixedIndexResolutionTests.class })
@SuppressWarnings("unused")
public class MongoPersistentEntityIndexResolverUnitTests {
@ -601,7 +603,7 @@ public class MongoPersistentEntityIndexResolverUnitTests { @@ -601,7 +603,7 @@ public class MongoPersistentEntityIndexResolverUnitTests {
public void compoundIndexDoesNotSpecifyNameWhenUsingGenerateName() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
ComountIndexWithAutogeneratedName.class);
CompoundIndexWithAutogeneratedName.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(indexDefinition.getIndexOptions())
@ -766,10 +768,10 @@ public class MongoPersistentEntityIndexResolverUnitTests { @@ -766,10 +768,10 @@ public class MongoPersistentEntityIndexResolverUnitTests {
class IndexDefinedOnSuperClass extends CompoundIndexOnLevelZero {}
@Document("ComountIndexWithAutogeneratedName")
@Document("CompoundIndexWithAutogeneratedName")
@CompoundIndexes({ @CompoundIndex(useGeneratedName = true, def = "{'foo': 1, 'bar': -1}", background = true,
sparse = true, unique = true) })
class ComountIndexWithAutogeneratedName {}
class CompoundIndexWithAutogeneratedName {}
@Document("WithComposedAnnotation")
@ComposedCompoundIndex
@ -829,6 +831,150 @@ public class MongoPersistentEntityIndexResolverUnitTests { @@ -829,6 +831,150 @@ public class MongoPersistentEntityIndexResolverUnitTests {
class WithEvaluatedCollationFromCompoundIndex {}
}
/**
* Test resolution of {@link CompoundWildcardIndexed}.
*
* @author Julia Lee
*/
public static class CompoundWildcardIndexResolutionTests {
@Test // GH-4471
public void compoundWildcardIndexOnSingleField() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
CompoundWildcardIndexOnFields.class);
assertThat(indexDefinitions).hasSize(1);
assertIndexPathAndCollection(new String[] { "foo.$**", "bar", "baz" }, "CompoundWildcardIndexOnSingleField",
indexDefinitions.get(0));
}
@Test // GH-4471
public void compoundWildcardIndexOnEntityWithProjection() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
CompoundWildcardIndexOnEntity.class);
assertThat(indexDefinitions).hasSize(1);
assertThat(indexDefinitions.get(0)).satisfies(it -> {
assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("$**", 1).append("bar", -1));
assertThat(it.getIndexOptions()).containsEntry("wildcardProjection",
org.bson.Document.parse("{'foo.something' : 0}"));
});
}
@Test // GH-4471
public void compoundWildcardIndexWithOptions() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
CompoundWildcardIndexWithOptions.class);
assertThat(indexDefinitions).hasSize(1);
assertThat(indexDefinitions.get(0)).satisfies(it -> {
assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("$**", 1).append("foo", 1));
org.bson.Document indexOptions = it.getIndexOptions();
assertThat(indexOptions).containsEntry("name", "my_index_name");
assertThat(indexOptions).containsEntry("wildcardProjection", org.bson.Document.parse("{'bar.something' : 1}"));
assertThat(indexOptions).containsEntry("collation",
new org.bson.Document().append("locale", "en_US").append("strength", 2));
assertThat(indexOptions).containsEntry("partialFilterExpression",
org.bson.Document.parse("{'value': {'$exists': true}}"));
});
}
@Test // GH-4471
public void compoundWildcardIndexWithCollationFromDocumentAnnotation() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
CompoundWildcardIndexWithCollationOnDocument.class);
assertThat(indexDefinitions.get(0)).satisfies(it -> {
assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo.$**", 1).append("bar", 1));
assertThat(it.getIndexOptions()).containsEntry("collation",
new org.bson.Document().append("locale", "en_US").append("strength", 2));
});
}
@Test // GH-4471
public void compoundWildcardIndexWithEvaluatedCollationFromAnnotation() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
CompoundWildcardIndexWithEvaluatedCollation.class);
assertThat(indexDefinitions.get(0)).satisfies(it -> {
assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo.$**", 1).append("bar", 1));
assertThat(it.getIndexOptions()).containsEntry("collation", new org.bson.Document().append("locale", "de_AT"));
});
}
@Test // GH-4471
public void rejectsWildcardProjectionOnSingleField() {
assertThatExceptionOfType(MappingException.class).isThrownBy(() ->
prepareMappingContextAndResolveIndexForType(IncorrectCompoundWildcardIndexOnFieldWithProjection.class));
}
@Test // GH-4471
public void requiresWildcardProjectionOnEntireEntity() {
assertThatExceptionOfType(MappingException.class).isThrownBy(() ->
prepareMappingContextAndResolveIndexForType(IncorrectCompoundWildcardIndexOnEntityWithoutProjection.class));
}
@Test // GH-4471
public void resolvesMultipleIndexesWithCompoundWildcardIndex() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
MultipleIndexes.class);
assertThat(indexDefinitions).hasSize(2);
assertThat(indexDefinitions.get(0).getIndexDefinition()).isInstanceOf(CompoundWildcardIndexDefinition.class);
assertThat(indexDefinitions.get(1).getIndexDefinition()).isInstanceOf(Index.class);
assertThat(indexDefinitions.get(0).getIndexKeys()).isEqualTo(new org.bson.Document().append("foo.$**", 1)
.append("bar", 1));
assertThat(indexDefinitions.get(1).getIndexKeys()).isEqualTo(new org.bson.Document().append("one", 1));
}
@Document("CompoundWildcardIndexOnSingleField")
@CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1, 'baz': 1}")
class CompoundWildcardIndexOnFields {}
@Document
@CompoundWildcardIndexed(wildcardFieldName = "foo", wildcardProjection = "{}", fields = "{'bar': 1}")
class IncorrectCompoundWildcardIndexOnFieldWithProjection {}
@Document
@CompoundWildcardIndexed(fields = "{ 'bar': 1 }")
class IncorrectCompoundWildcardIndexOnEntityWithoutProjection {}
@Document
@CompoundWildcardIndexed(wildcardProjection = "{'foo.something' : 0}", fields = "{'bar': -1}")
class CompoundWildcardIndexOnEntity {}
@Document
@CompoundWildcardIndexed(fields = "{'foo': 1}", wildcardProjection = "{'bar.something': 1}", name = "my_index_name",
collation = "{'locale': 'en_US', 'strength': 2}", partialFilter = "{'value': {'$exists': true}}")
class CompoundWildcardIndexWithOptions {}
@Document(collation = "{'locale': 'en_US', 'strength': 2}")
@CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1}")
class CompoundWildcardIndexWithCollationOnDocument {}
@Document(collation = "{'locale': 'en_US', 'strength': 2}")
@CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1}", collation = "#{{'locale' : 'de' + '_' + 'AT'}}")
class CompoundWildcardIndexWithEvaluatedCollation {}
@Document
@CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1}")
class MultipleIndexes {
@Indexed String one;
}
}
public static class TextIndexedResolutionTests {
@Test // DATAMONGO-937

Loading…
Cancel
Save