Skip to content

Commit fe1ab60

Browse files
DATAMONGO-1682 - Add support partialFilterExpression for reactive index creation.
We now support partial filter expression on indexes via Index.partial(…) on the reactive API. This allows to create partial indexes that only index the documents in a collection that meet a specified filter expression.
1 parent 3803945 commit fe1ab60

File tree

4 files changed

+170
-19
lines changed

4 files changed

+170
-19
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,60 +15,114 @@
1515
*/
1616
package org.springframework.data.mongodb.core;
1717

18+
import reactor.core.publisher.Flux;
19+
import reactor.core.publisher.Mono;
20+
21+
import java.util.Collection;
22+
import java.util.Optional;
23+
1824
import org.bson.Document;
25+
import org.springframework.data.mongodb.core.convert.QueryMapper;
1926
import org.springframework.data.mongodb.core.index.IndexDefinition;
2027
import org.springframework.data.mongodb.core.index.IndexInfo;
28+
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
2129
import org.springframework.util.Assert;
2230

31+
import com.mongodb.client.model.IndexOptions;
2332
import com.mongodb.reactivestreams.client.ListIndexesPublisher;
2433

25-
import reactor.core.publisher.Flux;
26-
import reactor.core.publisher.Mono;
27-
2834
/**
2935
* Default implementation of {@link IndexOperations}.
3036
*
3137
* @author Mark Paluch
32-
* @since 1.11
38+
* @author Christoph Strobl
39+
* @since 2.0
3340
*/
3441
public class DefaultReactiveIndexOperations implements ReactiveIndexOperations {
3542

43+
private static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression";
44+
3645
private final ReactiveMongoOperations mongoOperations;
3746
private final String collectionName;
47+
private final QueryMapper queryMapper;
48+
private final Optional<Class<?>> type;
49+
50+
/**
51+
* Creates a new {@link DefaultReactiveIndexOperations}.
52+
*
53+
* @param mongoOperations must not be {@literal null}.
54+
* @param collectionName must not be {@literal null}.
55+
*/
56+
public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName,
57+
QueryMapper queryMapper) {
58+
59+
this(mongoOperations, collectionName, queryMapper, null);
60+
}
3861

3962
/**
4063
* Creates a new {@link DefaultReactiveIndexOperations}.
4164
*
4265
* @param mongoOperations must not be {@literal null}.
4366
* @param collectionName must not be {@literal null}.
4467
*/
45-
public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName) {
68+
public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName,
69+
QueryMapper queryMapper, Class<?> type) {
4670

4771
Assert.notNull(mongoOperations, "ReactiveMongoOperations must not be null!");
4872
Assert.notNull(collectionName, "Collection must not be null!");
73+
Assert.notNull(queryMapper, "QueryMapper must not be null!");
4974

5075
this.mongoOperations = mongoOperations;
5176
this.collectionName = collectionName;
77+
this.queryMapper = queryMapper;
78+
this.type = Optional.ofNullable(type);
5279
}
5380

5481
/* (non-Javadoc)
5582
* @see org.springframework.data.mongodb.core.ReactiveIndexOperations#ensureIndex(org.springframework.data.mongodb.core.index.IndexDefinition)
5683
*/
5784
public Mono<String> ensureIndex(final IndexDefinition indexDefinition) {
5885

59-
return mongoOperations.execute(collectionName, (ReactiveCollectionCallback<String>) collection -> {
86+
return mongoOperations.execute(collectionName, collection -> {
6087

6188
Document indexOptions = indexDefinition.getIndexOptions();
6289

6390
if (indexOptions != null) {
64-
return collection.createIndex(indexDefinition.getIndexKeys(),
65-
IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition));
91+
92+
IndexOptions ops = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition);
93+
94+
if (indexOptions.containsKey(PARTIAL_FILTER_EXPRESSION_KEY)) {
95+
96+
Assert.isInstanceOf(Document.class, indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY));
97+
98+
MongoPersistentEntity<?> entity = type
99+
.map(val -> (MongoPersistentEntity) queryMapper.getMappingContext().getRequiredPersistentEntity(val))
100+
.orElseGet(() -> lookupPersistentEntity(collectionName));
101+
102+
ops = ops.partialFilterExpression(
103+
queryMapper.getMappedObject((Document) indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity));
104+
}
105+
106+
return collection.createIndex(indexDefinition.getIndexKeys(), ops);
66107
}
67108

68109
return collection.createIndex(indexDefinition.getIndexKeys());
69110
}).next();
70111
}
71112

113+
private MongoPersistentEntity<?> lookupPersistentEntity(String collection) {
114+
115+
Collection<? extends MongoPersistentEntity<?>> entities = queryMapper.getMappingContext().getPersistentEntities();
116+
117+
for (MongoPersistentEntity<?> entity : entities) {
118+
if (entity.getCollection().equals(collection)) {
119+
return entity;
120+
}
121+
}
122+
123+
return null;
124+
}
125+
72126
/* (non-Javadoc)
73127
* @see org.springframework.data.mongodb.core.ReactiveIndexOperations#dropIndex(java.lang.String)
74128
*/
@@ -77,7 +131,7 @@ public Mono<Void> dropIndex(final String name) {
77131
return mongoOperations.execute(collectionName, collection -> {
78132

79133
return Mono.from(collection.dropIndex(name));
80-
}).flatMap(success -> Mono.<Void>empty()).next();
134+
}).flatMap(success -> Mono.<Void> empty()).next();
81135
}
82136

83137
/* (non-Javadoc)

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveIndexOperations.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,12 @@
1515
*/
1616
package org.springframework.data.mongodb.core;
1717

18-
import java.util.List;
18+
import reactor.core.publisher.Flux;
19+
import reactor.core.publisher.Mono;
1920

2021
import org.springframework.data.mongodb.core.index.IndexDefinition;
2122
import org.springframework.data.mongodb.core.index.IndexInfo;
2223

23-
import reactor.core.publisher.Flux;
24-
import reactor.core.publisher.Mono;
25-
2624
/**
2725
* Index operations on a collection.
2826
*

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,14 +310,14 @@ public MongoConverter getConverter() {
310310
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#reactiveIndexOps(java.lang.String)
311311
*/
312312
public ReactiveIndexOperations indexOps(String collectionName) {
313-
return new DefaultReactiveIndexOperations(this, collectionName);
313+
return new DefaultReactiveIndexOperations(this, collectionName, this.queryMapper);
314314
}
315315

316316
/* (non-Javadoc)
317317
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#reactiveIndexOps(java.lang.Class)
318318
*/
319319
public ReactiveIndexOperations indexOps(Class<?> entityClass) {
320-
return new DefaultReactiveIndexOperations(this, determineCollectionName(entityClass));
320+
return new DefaultReactiveIndexOperations(this, determineCollectionName(entityClass), this.queryMapper);
321321
}
322322

323323
public String getCollectionName(Class<?> entityClass) {

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.hamcrest.core.Is.*;
2020
import static org.junit.Assume.*;
21+
import static org.springframework.data.mongodb.core.index.PartialIndexFilter.*;
22+
import static org.springframework.data.mongodb.core.query.Criteria.*;
2123

24+
import reactor.core.publisher.Mono;
2225
import reactor.test.StepVerifier;
2326

27+
import java.util.function.Predicate;
28+
2429
import org.bson.Document;
2530
import org.junit.Before;
2631
import org.junit.Test;
@@ -30,9 +35,11 @@
3035
import org.springframework.data.domain.Sort.Direction;
3136
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
3237
import org.springframework.data.mongodb.core.Collation.CaseFirst;
33-
import org.springframework.data.mongodb.core.DefaultIndexOperationsIntegrationTests.DefaultIndexOperationsIntegrationTestsSample;
38+
import org.springframework.data.mongodb.core.convert.QueryMapper;
3439
import org.springframework.data.mongodb.core.index.Index;
3540
import org.springframework.data.mongodb.core.index.IndexDefinition;
41+
import org.springframework.data.mongodb.core.index.IndexInfo;
42+
import org.springframework.data.mongodb.core.mapping.Field;
3643
import org.springframework.data.util.Version;
3744
import org.springframework.test.context.ContextConfiguration;
3845
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -63,6 +70,7 @@ protected String getDatabaseName() {
6370
}
6471
}
6572

73+
private static final Version THREE_DOT_TWO = new Version(3, 2);
6674
private static final Version THREE_DOT_FOUR = new Version(3, 4);
6775
private static Version mongoVersion;
6876

@@ -78,8 +86,10 @@ public void setUp() {
7886
String collectionName = this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class);
7987

8088
this.collection = this.template.getMongoDatabase().getCollection(collectionName, Document.class);
81-
this.collection.dropIndexes();
82-
this.indexOps = new DefaultReactiveIndexOperations(template, collectionName);
89+
Mono.from(this.collection.dropIndexes()).subscribe();
90+
91+
this.indexOps = new DefaultReactiveIndexOperations(template, collectionName,
92+
new QueryMapper(template.getConverter()));
8393
}
8494

8595
private void queryMongoVersionIfNecessary() {
@@ -110,7 +120,7 @@ public void shouldCreateIndexWithCollationCorrectly() {
110120
.append("normalization", false) //
111121
.append("backwards", false);
112122

113-
StepVerifier.create(indexOps.getIndexInfo().filter(val -> val.getName().equals("with-collation")))
123+
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("with-collation"))) //
114124
.consumeNextWith(indexInfo -> {
115125

116126
assertThat(indexInfo.getCollation()).isPresent();
@@ -121,7 +131,96 @@ public void shouldCreateIndexWithCollationCorrectly() {
121131

122132
assertThat(result).isEqualTo(expected);
123133
}) //
134+
.thenAwait();
135+
}
136+
137+
@Test // DATAMONGO-1682
138+
public void shouldApplyPartialFilterCorrectly() {
139+
140+
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true));
141+
142+
IndexDefinition id = new Index().named("partial-with-criteria").on("k3y", Direction.ASC)
143+
.partial(of(where("q-t-y").gte(10)));
144+
145+
indexOps.ensureIndex(id).subscribe();
146+
147+
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("partial-with-criteria"))) //
148+
.consumeNextWith(indexInfo -> {
149+
assertThat(indexInfo.getPartialFilterExpression()).isEqualTo("{ \"q-t-y\" : { \"$gte\" : 10 } }");
150+
}) //
151+
.thenAwait();
152+
}
153+
154+
@Test // DATAMONGO-1682
155+
public void shouldApplyPartialFilterWithMappedPropertyCorrectly() {
156+
157+
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true));
158+
159+
IndexDefinition id = new Index().named("partial-with-mapped-criteria").on("k3y", Direction.ASC)
160+
.partial(of(where("quantity").gte(10)));
161+
162+
indexOps.ensureIndex(id).subscribe();
163+
164+
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("partial-with-mapped-criteria"))) //
165+
.consumeNextWith(indexInfo -> {
166+
assertThat(indexInfo.getPartialFilterExpression()).isEqualTo("{ \"qty\" : { \"$gte\" : 10 } }");
167+
}).thenAwait();
168+
}
169+
170+
@Test // DATAMONGO-1682
171+
public void shouldApplyPartialDBOFilterCorrectly() {
172+
173+
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true));
174+
175+
IndexDefinition id = new Index().named("partial-with-dbo").on("k3y", Direction.ASC)
176+
.partial(of(new org.bson.Document("qty", new org.bson.Document("$gte", 10))));
177+
178+
indexOps.ensureIndex(id).subscribe();
179+
180+
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("partial-with-dbo"))) //
181+
.consumeNextWith(indexInfo -> {
182+
assertThat(indexInfo.getPartialFilterExpression()).isEqualTo("{ \"qty\" : { \"$gte\" : 10 } }");
183+
}) //
184+
.thenAwait();
185+
186+
}
187+
188+
@Test // DATAMONGO-1682
189+
public void shouldFavorExplicitMappingHintViaClass() {
190+
191+
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true));
192+
193+
IndexDefinition id = new Index().named("partial-with-inheritance").on("k3y", Direction.ASC)
194+
.partial(of(where("age").gte(10)));
195+
196+
indexOps = new DefaultReactiveIndexOperations(template,
197+
this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class),
198+
new QueryMapper(template.getConverter()), MappingToSameCollection.class);
199+
200+
indexOps.ensureIndex(id).subscribe();
201+
202+
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("partial-with-inheritance"))) //
203+
.consumeNextWith(indexInfo -> {
204+
assertThat(indexInfo.getPartialFilterExpression()).isEqualTo("{ \"a_g_e\" : { \"$gte\" : 10 } }");
205+
}) //
124206
.verifyComplete();
125207
}
126208

209+
Predicate<IndexInfo> indexByName(String name) {
210+
return indexInfo -> indexInfo.getName().equals(name);
211+
}
212+
213+
@org.springframework.data.mongodb.core.mapping.Document(collection = "default-index-operations-tests")
214+
static class DefaultIndexOperationsIntegrationTestsSample {
215+
216+
@Field("qty") Integer quantity;
217+
}
218+
219+
@org.springframework.data.mongodb.core.mapping.Document(collection = "default-index-operations-tests")
220+
static class MappingToSameCollection
221+
extends DefaultIndexOperationsIntegrationTests.DefaultIndexOperationsIntegrationTestsSample {
222+
223+
@Field("a_g_e") Integer age;
224+
}
225+
127226
}

0 commit comments

Comments
 (0)