From d5006bb69351508ee20b3345a4b2f5b088809107 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 2 May 2017 08:20:51 +0200 Subject: [PATCH] DATAMONGO-1682 - Add support partialFilterExpression for reactive index creation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../core/DefaultReactiveIndexOperations.java | 72 ++++++++++-- .../mongodb/core/ReactiveIndexOperations.java | 4 +- .../mongodb/core/ReactiveMongoTemplate.java | 4 +- .../DefaultReactiveIndexOperationsTests.java | 107 +++++++++++++++++- 4 files changed, 169 insertions(+), 18 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java index 0ba81a33f..f19905b66 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java @@ -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> 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 ensureIndex(final IndexDefinition indexDefinition) { - return mongoOperations.execute(collectionName, (ReactiveCollectionCallback) 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> 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.empty()).next(); + }).flatMap(success -> Mono. empty()).next(); } /* (non-Javadoc) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveIndexOperations.java index 88c89d2c3..259ad13d1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveIndexOperations.java @@ -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; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 9ed9ae3f0..39862d413 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -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) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java index 1dbff6cf4..2494fbfc6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java @@ -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 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; + } + }