Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 332e63a

Browse files
sxhinzvcmarcingrzejszczak
authored andcommittedSep 13, 2024
Add support for Compound Wildcard Indexes.
See #4471
1 parent a84b3c2 commit 332e63a

File tree

4 files changed

+400
-6
lines changed

4 files changed

+400
-6
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2014-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.index;
17+
18+
import org.bson.Document;
19+
import org.springframework.util.Assert;
20+
21+
/**
22+
* {@link CompoundWildcardIndexDefinition} is a specific {@link Index} that includes one {@link WildcardIndex} and
23+
* one or more non-wildcard fields.
24+
*
25+
* @author Julia Lee
26+
* @since 4.2
27+
*/
28+
public class CompoundWildcardIndexDefinition extends WildcardIndex {
29+
30+
private final Document indexKeys;
31+
32+
/**
33+
* Creates a new {@link CompoundWildcardIndexDefinition} for the given {@literal wildcardPath} and {@literal keys}.
34+
* If {@literal wildcardPath} is empty, the wildcard index will apply to the root entity, using {@code $**}.
35+
* <br />
36+
*
37+
* @param wildcardPath can be a {@literal empty} {@link String}.
38+
*/
39+
public CompoundWildcardIndexDefinition(String wildcardPath, Document indexKeys) {
40+
41+
super(wildcardPath);
42+
this.indexKeys = indexKeys;
43+
}
44+
45+
@Override
46+
public Document getIndexKeys() {
47+
48+
Document document = new Document();
49+
document.putAll(indexKeys);
50+
document.putAll(super.getIndexKeys());
51+
return document;
52+
}
53+
54+
@Override
55+
public Document getIndexOptions() {
56+
57+
Document options = super.getIndexOptions();
58+
return options;
59+
}
60+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2011-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.index;
17+
18+
import org.springframework.core.annotation.AliasFor;
19+
20+
import java.lang.annotation.Documented;
21+
import java.lang.annotation.ElementType;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* Mark a class to use compound wildcard indexes. <br />
28+
*
29+
* <pre class="code">
30+
* &#64;Document
31+
* &#64;CompoundWildcardIndexed(wildcardFieldName = "address", fields = "{'firstname': 1}")
32+
* class Person {
33+
* String firstname;
34+
* Address address;
35+
* }
36+
*
37+
* db.product.createIndex({"address.$**": 1, "firstname": 1})
38+
* </pre>
39+
*
40+
* {@literal wildcardProjection} can be used to specify keys to in-/exclude in the index.
41+
*
42+
* <pre class="code">
43+
*
44+
* &#64;Document
45+
* &#64;CompoundWildcardIndexed(wildcardProjection = "{'address.zip': 0}", fields = "{'firstname': 1}")
46+
* class Person {
47+
* String firstname;
48+
* Address address;
49+
* }
50+
*
51+
* db.user.createIndex({"$**": 1, "firstname": 1}, {"wildcardProjection": {"address.zip": 0}})
52+
* </pre>
53+
*
54+
* @author Julia Lee
55+
*/
56+
@Target({ ElementType.TYPE })
57+
@Documented
58+
@WildcardIndexed
59+
@CompoundIndex
60+
@Retention(RetentionPolicy.RUNTIME)
61+
public @interface CompoundWildcardIndexed {
62+
63+
/**
64+
* The name of the sub-field to which a wildcard index is applied. If empty, the wildcard term will resolve to "$**".
65+
*
66+
* @return empty by default.
67+
*/
68+
String wildcardFieldName() default "";
69+
70+
/**
71+
* Explicitly specify sub-fields to be in-/excluded as a {@link org.bson.Document#parse(String) parsable} String.
72+
* <br />
73+
* <strong>NOTE:</strong> Can only be applied on when wildcard term is "$**"
74+
*
75+
* @return empty by default.
76+
*/
77+
@AliasFor(annotation = WildcardIndexed.class, attribute = "wildcardProjection")
78+
String wildcardProjection() default "";
79+
80+
/**
81+
* Definition of non-wildcard index(es) in JSON format, wherein the keys are the fields to be indexed and the values
82+
* define the index direction (1 for ascending, -1 for descending). <br />
83+
*
84+
* <pre class="code">
85+
* &#64;Document
86+
* &#64;CompoundWildcardIndexed(wildcardProjection = "{ 'address.zip' : 0 }", fields = "{'firstname': 1}")
87+
* class Person {
88+
* String firstname;
89+
* Address address;
90+
* }
91+
* </pre>
92+
*
93+
* @return empty String by default.
94+
*/
95+
@AliasFor(annotation = CompoundIndex.class, attribute = "def")
96+
String fields();
97+
98+
/**
99+
* Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template
100+
* expression}. <br />
101+
*
102+
* @return empty by default.
103+
*/
104+
@AliasFor(annotation = WildcardIndexed.class, attribute = "name")
105+
String name() default "";
106+
107+
/**
108+
* If set to {@literal true} then MongoDB will ignore the given index name and instead generate a new name. Defaults
109+
* to {@literal false}.
110+
*
111+
* @return {@literal false} by default
112+
*/
113+
@AliasFor(annotation = WildcardIndexed.class, attribute = "useGeneratedName")
114+
boolean useGeneratedName() default false;
115+
116+
/**
117+
* Only index the documents in a collection that meet a specified {@link IndexFilter filter expression}. <br />
118+
*
119+
* @return empty by default.
120+
*/
121+
@AliasFor(annotation = WildcardIndexed.class, attribute = "partialFilter")
122+
String partialFilter() default "";
123+
124+
/**
125+
* Defines the collation to apply.
126+
*
127+
* @return an empty {@link String} by default.
128+
*/
129+
@AliasFor(annotation = WildcardIndexed.class, attribute = "collation")
130+
String collation() default "";
131+
}

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

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
* @author Mark Paluch
8080
* @author Dave Perryman
8181
* @author Stefan Tirea
82+
* @author Julia Lee
8283
* @since 1.5
8384
*/
8485
public class MongoPersistentEntityIndexResolver implements IndexResolver {
@@ -129,6 +130,7 @@ public List<IndexDefinitionHolder> resolveIndexForEntity(MongoPersistentEntity<?
129130
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", collection, root));
130131
indexInformation.addAll(potentiallyCreateWildcardIndexDefinitions("", collection, root));
131132
indexInformation.addAll(potentiallyCreateTextIndexDefinition(root, collection));
133+
indexInformation.addAll(potentiallyCreateCompoundWildcardDefinition(root, collection));
132134

133135
root.doWithProperties((PropertyHandler<MongoPersistentProperty>) property -> this
134136
.potentiallyAddIndexForProperty(root, property, indexInformation, new CycleGuard()));
@@ -154,6 +156,22 @@ private void verifyWildcardIndexedProjection(MongoPersistentEntity<?> entity) {
154156
}
155157
}
156158
});
159+
160+
if (entity.isAnnotationPresent(CompoundWildcardIndexed.class)) {
161+
CompoundWildcardIndexed indexed = entity.getRequiredAnnotation(CompoundWildcardIndexed.class);
162+
163+
if (!ObjectUtils.isEmpty(indexed.wildcardFieldName()) && !ObjectUtils.isEmpty(indexed.wildcardProjection())) {
164+
165+
throw new MappingException(
166+
String.format("CompoundWildcardIndex.wildcardProjection is only allowed on \"$**\"; Offending property: %s",
167+
indexed.wildcardFieldName()));
168+
}
169+
170+
if (ObjectUtils.isEmpty(indexed.wildcardFieldName()) && ObjectUtils.isEmpty(indexed.wildcardProjection())) {
171+
172+
throw new MappingException(String.format("CompoundWildcardIndex.wildcardProjection is required on \"$**\""));
173+
}
174+
}
157175
}
158176

159177
private void potentiallyAddIndexForProperty(MongoPersistentEntity<?> root, MongoPersistentProperty persistentProperty,
@@ -280,7 +298,8 @@ private List<IndexDefinitionHolder> createIndexDefinitionHolderForProperty(Strin
280298
private List<IndexDefinitionHolder> potentiallyCreateCompoundIndexDefinitions(String dotPath, String collection,
281299
MongoPersistentEntity<?> entity) {
282300

283-
if (entity.findAnnotation(CompoundIndexes.class) == null && entity.findAnnotation(CompoundIndex.class) == null) {
301+
if ((!entity.isAnnotationPresent(CompoundIndexes.class) && !entity.isAnnotationPresent(CompoundIndex.class))
302+
|| entity.isAnnotationPresent(CompoundWildcardIndexed.class)) {
284303
return Collections.emptyList();
285304
}
286305

@@ -290,7 +309,8 @@ private List<IndexDefinitionHolder> potentiallyCreateCompoundIndexDefinitions(St
290309
private List<IndexDefinitionHolder> potentiallyCreateWildcardIndexDefinitions(String dotPath, String collection,
291310
MongoPersistentEntity<?> entity) {
292311

293-
if (!entity.isAnnotationPresent(WildcardIndexed.class)) {
312+
if (!entity.isAnnotationPresent(WildcardIndexed.class)
313+
|| entity.isAnnotationPresent(CompoundWildcardIndexed.class)) {
294314
return Collections.emptyList();
295315
}
296316

@@ -345,6 +365,19 @@ private Collection<? extends IndexDefinitionHolder> potentiallyCreateTextIndexDe
345365

346366
}
347367

368+
private Collection<? extends IndexDefinitionHolder> potentiallyCreateCompoundWildcardDefinition(
369+
MongoPersistentEntity<?> entity, String collection) {
370+
371+
if (!entity.isAnnotationPresent(CompoundWildcardIndexed.class)) {
372+
return Collections.emptyList();
373+
}
374+
375+
CompoundWildcardIndexed compoundWildcardIndex = entity.getRequiredAnnotation(CompoundWildcardIndexed.class);
376+
IndexDefinitionHolder compoundWildcardIndexDefinition = createCompoundWildcardIndexDefinition(collection,
377+
compoundWildcardIndex, entity);
378+
return Collections.singletonList(compoundWildcardIndexDefinition);
379+
}
380+
348381
private void appendTextIndexInformation(DotPath dotPath, Path path, TextIndexDefinitionBuilder indexDefinitionBuilder,
349382
MongoPersistentEntity<?> entity, TextIndexIncludeOptions includeOptions, CycleGuard guard) {
350383

@@ -483,6 +516,30 @@ protected IndexDefinitionHolder createWildcardIndexDefinition(String dotPath, St
483516
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
484517
}
485518

519+
protected IndexDefinitionHolder createCompoundWildcardIndexDefinition(String collection, CompoundWildcardIndexed index,
520+
@Nullable MongoPersistentEntity<?> entity) {
521+
522+
String wildcardField = index.wildcardFieldName();
523+
org.bson.Document indexKeys = resolveCompoundIndexKeyFromStringDefinition("", index.fields(), entity);
524+
525+
CompoundWildcardIndexDefinition indexDefinition = new CompoundWildcardIndexDefinition(wildcardField, indexKeys);
526+
527+
if (StringUtils.hasText(index.wildcardProjection()) && ObjectUtils.isEmpty(wildcardField)) {
528+
indexDefinition.wildcardProjection(evaluateWildcardProjection(index.wildcardProjection(), entity));
529+
}
530+
531+
if (StringUtils.hasText(index.partialFilter())) {
532+
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity));
533+
}
534+
535+
if (!index.useGeneratedName()) {
536+
indexDefinition.named(pathAwareIndexName(index.name(), "", entity, null));
537+
}
538+
539+
indexDefinition.collation(resolveCollation(index, entity));
540+
return new IndexDefinitionHolder("", indexDefinition, collection);
541+
}
542+
486543
private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString,
487544
PersistentEntity<?, ?> entity) {
488545

0 commit comments

Comments
 (0)
Please sign in to comment.