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.

Original pull request: #474.
This commit is contained in:
Christoph Strobl
2017-05-02 08:20:51 +02:00
committed by Mark Paluch
parent 82fdbe8cc2
commit d5006bb693
4 changed files with 169 additions and 18 deletions

View File

@@ -15,27 +15,38 @@
*/
package org.springframework.data.mongodb.core;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.Optional;
import org.bson.Document;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.index.IndexDefinition;
import org.springframework.data.mongodb.core.index.IndexInfo;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.util.Assert;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.reactivestreams.client.ListIndexesPublisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Default implementation of {@link IndexOperations}.
*
* @author Mark Paluch
* @since 1.11
* @author Christoph Strobl
* @since 2.0
*/
public class DefaultReactiveIndexOperations implements ReactiveIndexOperations {
private static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression";
private final ReactiveMongoOperations mongoOperations;
private final String collectionName;
private final QueryMapper queryMapper;
private final Optional<Class<?>> type;
/**
* Creates a new {@link DefaultReactiveIndexOperations}.
@@ -43,13 +54,29 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations {
* @param mongoOperations must not be {@literal null}.
* @param collectionName must not be {@literal null}.
*/
public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName) {
public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName,
QueryMapper queryMapper) {
this(mongoOperations, collectionName, queryMapper, null);
}
/**
* Creates a new {@link DefaultReactiveIndexOperations}.
*
* @param mongoOperations must not be {@literal null}.
* @param collectionName must not be {@literal null}.
*/
public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName,
QueryMapper queryMapper, Class<?> type) {
Assert.notNull(mongoOperations, "ReactiveMongoOperations must not be null!");
Assert.notNull(collectionName, "Collection must not be null!");
Assert.notNull(queryMapper, "QueryMapper must not be null!");
this.mongoOperations = mongoOperations;
this.collectionName = collectionName;
this.queryMapper = queryMapper;
this.type = Optional.ofNullable(type);
}
/* (non-Javadoc)
@@ -57,19 +84,46 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations {
*/
public Mono<String> ensureIndex(final IndexDefinition indexDefinition) {
return mongoOperations.execute(collectionName, (ReactiveCollectionCallback<String>) collection -> {
return mongoOperations.execute(collectionName, collection -> {
Document indexOptions = indexDefinition.getIndexOptions();
if (indexOptions != null) {
return collection.createIndex(indexDefinition.getIndexKeys(),
IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition));
IndexOptions ops = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition);
if (indexOptions.containsKey(PARTIAL_FILTER_EXPRESSION_KEY)) {
Assert.isInstanceOf(Document.class, indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY));
MongoPersistentEntity<?> entity = type
.map(val -> (MongoPersistentEntity) queryMapper.getMappingContext().getRequiredPersistentEntity(val))
.orElseGet(() -> lookupPersistentEntity(collectionName));
ops = ops.partialFilterExpression(
queryMapper.getMappedObject((Document) indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity));
}
return collection.createIndex(indexDefinition.getIndexKeys(), ops);
}
return collection.createIndex(indexDefinition.getIndexKeys());
}).next();
}
private MongoPersistentEntity<?> lookupPersistentEntity(String collection) {
Collection<? extends MongoPersistentEntity<?>> entities = queryMapper.getMappingContext().getPersistentEntities();
for (MongoPersistentEntity<?> entity : entities) {
if (entity.getCollection().equals(collection)) {
return entity;
}
}
return null;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveIndexOperations#dropIndex(java.lang.String)
*/
@@ -78,7 +132,7 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations {
return mongoOperations.execute(collectionName, collection -> {
return Mono.from(collection.dropIndex(name));
}).flatMap(success -> Mono.<Void>empty()).next();
}).flatMap(success -> Mono.<Void> empty()).next();
}
/* (non-Javadoc)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* Copyright 2016-2017 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.
@@ -15,8 +15,6 @@
*/
package org.springframework.data.mongodb.core;
import java.util.List;
import org.springframework.data.mongodb.core.index.IndexDefinition;
import org.springframework.data.mongodb.core.index.IndexInfo;

View File

@@ -302,14 +302,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#reactiveIndexOps(java.lang.String)
*/
public ReactiveIndexOperations indexOps(String collectionName) {
return new DefaultReactiveIndexOperations(this, collectionName);
return new DefaultReactiveIndexOperations(this, collectionName, this.queryMapper);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#reactiveIndexOps(java.lang.Class)
*/
public ReactiveIndexOperations indexOps(Class<?> entityClass) {
return new DefaultReactiveIndexOperations(this, determineCollectionName(entityClass));
return new DefaultReactiveIndexOperations(this, determineCollectionName(entityClass), this.queryMapper);
}
public String getCollectionName(Class<?> entityClass) {

View File

@@ -18,9 +18,14 @@ package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.hamcrest.core.Is.*;
import static org.junit.Assume.*;
import static org.springframework.data.mongodb.core.index.PartialIndexFilter.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.function.Predicate;
import org.bson.Document;
import org.junit.Before;
import org.junit.Test;
@@ -31,9 +36,11 @@ import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Collation.CaseFirst;
import org.springframework.data.mongodb.core.DefaultIndexOperationsIntegrationTests.DefaultIndexOperationsIntegrationTestsSample;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexDefinition;
import org.springframework.data.mongodb.core.index.IndexInfo;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.util.Version;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -64,6 +71,7 @@ public class DefaultReactiveIndexOperationsTests {
}
}
private static final Version THREE_DOT_TWO = new Version(3, 2);
private static final Version THREE_DOT_FOUR = new Version(3, 4);
private static Version mongoVersion;
@@ -79,8 +87,10 @@ public class DefaultReactiveIndexOperationsTests {
String collectionName = this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class);
this.collection = this.template.getMongoDatabase().getCollection(collectionName, Document.class);
this.collection.dropIndexes();
this.indexOps = new DefaultReactiveIndexOperations(template, collectionName);
Mono.from(this.collection.dropIndexes()).subscribe();
this.indexOps = new DefaultReactiveIndexOperations(template, collectionName,
new QueryMapper(template.getConverter()));
}
private void queryMongoVersionIfNecessary() {
@@ -111,7 +121,7 @@ public class DefaultReactiveIndexOperationsTests {
.append("normalization", false) //
.append("backwards", false);
StepVerifier.create(indexOps.getIndexInfo().filter(val -> val.getName().equals("with-collation")))
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("with-collation"))) //
.consumeNextWith(indexInfo -> {
assertThat(indexInfo.getCollation()).isPresent();
@@ -122,7 +132,96 @@ public class DefaultReactiveIndexOperationsTests {
assertThat(result).isEqualTo(expected);
}) //
.thenAwait();
}
@Test // DATAMONGO-1682
public void shouldApplyPartialFilterCorrectly() {
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true));
IndexDefinition id = new Index().named("partial-with-criteria").on("k3y", Direction.ASC)
.partial(of(where("q-t-y").gte(10)));
indexOps.ensureIndex(id).subscribe();
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("partial-with-criteria"))) //
.consumeNextWith(indexInfo -> {
assertThat(indexInfo.getPartialFilterExpression()).isEqualTo("{ \"q-t-y\" : { \"$gte\" : 10 } }");
}) //
.thenAwait();
}
@Test // DATAMONGO-1682
public void shouldApplyPartialFilterWithMappedPropertyCorrectly() {
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true));
IndexDefinition id = new Index().named("partial-with-mapped-criteria").on("k3y", Direction.ASC)
.partial(of(where("quantity").gte(10)));
indexOps.ensureIndex(id).subscribe();
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("partial-with-mapped-criteria"))) //
.consumeNextWith(indexInfo -> {
assertThat(indexInfo.getPartialFilterExpression()).isEqualTo("{ \"qty\" : { \"$gte\" : 10 } }");
}).thenAwait();
}
@Test // DATAMONGO-1682
public void shouldApplyPartialDBOFilterCorrectly() {
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true));
IndexDefinition id = new Index().named("partial-with-dbo").on("k3y", Direction.ASC)
.partial(of(new org.bson.Document("qty", new org.bson.Document("$gte", 10))));
indexOps.ensureIndex(id).subscribe();
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("partial-with-dbo"))) //
.consumeNextWith(indexInfo -> {
assertThat(indexInfo.getPartialFilterExpression()).isEqualTo("{ \"qty\" : { \"$gte\" : 10 } }");
}) //
.thenAwait();
}
@Test // DATAMONGO-1682
public void shouldFavorExplicitMappingHintViaClass() {
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true));
IndexDefinition id = new Index().named("partial-with-inheritance").on("k3y", Direction.ASC)
.partial(of(where("age").gte(10)));
indexOps = new DefaultReactiveIndexOperations(template,
this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class),
new QueryMapper(template.getConverter()), MappingToSameCollection.class);
indexOps.ensureIndex(id).subscribe();
StepVerifier.create(indexOps.getIndexInfo().filter(this.indexByName("partial-with-inheritance"))) //
.consumeNextWith(indexInfo -> {
assertThat(indexInfo.getPartialFilterExpression()).isEqualTo("{ \"a_g_e\" : { \"$gte\" : 10 } }");
}) //
.verifyComplete();
}
Predicate<IndexInfo> indexByName(String name) {
return indexInfo -> indexInfo.getName().equals(name);
}
@org.springframework.data.mongodb.core.mapping.Document(collection = "default-index-operations-tests")
static class DefaultIndexOperationsIntegrationTestsSample {
@Field("qty") Integer quantity;
}
@org.springframework.data.mongodb.core.mapping.Document(collection = "default-index-operations-tests")
static class MappingToSameCollection
extends DefaultIndexOperationsIntegrationTests.DefaultIndexOperationsIntegrationTestsSample {
@Field("a_g_e") Integer age;
}
}