From 9a0241992ea9baac764de2c5f7584b9febfc0ab3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 19 Dec 2016 08:24:05 +0100 Subject: [PATCH] DATAMONGO-1467 - Add support for MongoDB 3.2 partialFilterExpression for 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(…). This allows to create partial indexes that only index the documents in a collection that meet a specified filter expression. new Index().named("idx").on("k3y", ASC).partial(filter(where("age").gte(10))) The filter expression can be set via a plain DBObject or a CriteriaDefinition and is mapped against the associated domain type. Original pull request: #431. --- .../config/MappingMongoConverterParser.java | 2 + .../mongodb/core/DefaultIndexOperations.java | 59 ++++++++- .../core/DefaultIndexOperationsProvider.java | 8 +- .../data/mongodb/core/IndexConverters.java | 42 +------ .../mongodb/core/IndexOperationsProvider.java | 2 + .../data/mongodb/core/MongoTemplate.java | 6 +- .../mongodb/core/convert/QueryMapper.java | 4 + .../mongodb/core/index/GeospatialIndex.java | 20 +++ .../data/mongodb/core/index/Index.java | 20 +++ .../data/mongodb/core/index/IndexFilter.java | 36 ++++++ .../data/mongodb/core/index/IndexInfo.java | 80 +++++++++++- .../core/index/PartialIndexFilter.java | 74 +++++++++++ .../core/index/TextIndexDefinition.java | 20 +++ ...efaultIndexOperationsIntegrationTests.java | 118 ++++++++++++++++-- 14 files changed, 437 insertions(+), 54 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexFilter.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/PartialIndexFilter.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java index fb1381e16..69b396bdb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java @@ -55,6 +55,7 @@ import org.springframework.data.mapping.context.MappingContextIsNewStrategyFacto import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy; import org.springframework.data.mongodb.core.convert.CustomConversions; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -125,6 +126,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser { BeanDefinitionBuilder indexOperationsProviderBuilder = BeanDefinitionBuilder.genericBeanDefinition("org.springframework.data.mongodb.core.DefaultIndexOperationsProvider"); indexOperationsProviderBuilder.addConstructorArgReference(dbFactoryRef); + indexOperationsProviderBuilder.addConstructorArgValue(BeanDefinitionBuilder.genericBeanDefinition(QueryMapper.class).addConstructorArgReference(id).getBeanDefinition()); parserContext.registerBeanComponent(new BeanComponentDefinition(indexOperationsProviderBuilder.getBeanDefinition(), "indexOperationsProvider")); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java index 2f3f8f4c3..e190143cf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java @@ -18,13 +18,17 @@ package org.springframework.data.mongodb.core; import static org.springframework.data.mongodb.core.MongoTemplate.*; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import org.bson.Document; import org.springframework.dao.DataAccessException; +import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.util.Assert; import com.mongodb.MongoException; @@ -43,22 +47,45 @@ import com.mongodb.client.model.IndexOptions; */ public class DefaultIndexOperations implements IndexOperations { + public static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression"; + private final MongoDbFactory mongoDbFactory; private final String collectionName; + private final QueryMapper mapper; + private final Class type; /** * Creates a new {@link DefaultIndexOperations}. * * @param mongoDbFactory must not be {@literal null}. * @param collectionName must not be {@literal null}. + * @param queryMapper must not be {@literal null}. */ - public DefaultIndexOperations(MongoDbFactory mongoDbFactory, String collectionName) { + public DefaultIndexOperations(MongoDbFactory mongoDbFactory, String collectionName, QueryMapper queryMapper) { + + this(mongoDbFactory, collectionName, queryMapper, null); + } + + /** + * Creates a new {@link DefaultIndexOperations}. + * + * @param mongoDbFactory must not be {@literal null}. + * @param collectionName must not be {@literal null}. + * @param queryMapper must not be {@literal null}. + * @param type Type used for mapping potential partial index filter expression. Can be {@literal null}. + * @since 1.10 + */ + public DefaultIndexOperations(MongoDbFactory mongoDbFactory, String collectionName, QueryMapper queryMapper, + Class type) { Assert.notNull(mongoDbFactory, "MongoDbFactory must not be null!"); Assert.notNull(collectionName, "Collection name can not be null!"); + Assert.notNull(queryMapper, "QueryMapper must not be null!"); this.mongoDbFactory = mongoDbFactory; this.collectionName = collectionName; + this.mapper = queryMapper; + this.type = type; } /* @@ -74,10 +101,38 @@ public class DefaultIndexOperations implements IndexOperations { if (indexOptions != null) { IndexOptions ops = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition); + + if (indexOptions.containsKey(PARTIAL_FILTER_EXPRESSION_KEY)) { + + Assert.isInstanceOf(Document.class, indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY)); + + ops.partialFilterExpression( mapper.getMappedObject( + (Document) indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), lookupPersistentEntity(type, collectionName))); + } + return collection.createIndex(indexDefinition.getIndexKeys(), ops); } return collection.createIndex(indexDefinition.getIndexKeys()); - }); + } + + ); + } + + private MongoPersistentEntity lookupPersistentEntity(Class entityType, String collection) { + + if (entityType != null) { + return mapper.getMappingContext().getPersistentEntity(entityType); + } + + Collection> entities = mapper.getMappingContext().getPersistentEntities(); + + for (MongoPersistentEntity entity : entities) { + if (entity.getCollection().equals(collection)) { + return entity; + } + } + + return null; } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java index f01f95670..79ffe6c35 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core; import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.convert.QueryMapper; /** * {@link IndexOperationsProvider} to obtain {@link IndexOperations} from a given {@link MongoDbFactory}. TODO: Review @@ -27,12 +28,13 @@ import org.springframework.data.mongodb.MongoDbFactory; class DefaultIndexOperationsProvider implements IndexOperationsProvider { private final MongoDbFactory mongoDbFactory; + private final QueryMapper mapper; /** * @param mongoDbFactory must not be {@literal null}. */ - DefaultIndexOperationsProvider(MongoDbFactory mongoDbFactory) { - this.mongoDbFactory = mongoDbFactory; + DefaultIndexOperationsProvider(MongoDbFactory mongoDbFactory, QueryMapper mapper) { + this.mongoDbFactory = mongoDbFactory; this.mapper = mapper; } /* (non-Javadoc) @@ -40,6 +42,6 @@ class DefaultIndexOperationsProvider implements IndexOperationsProvider { */ @Override public IndexOperations indexOps(String collectionName) { - return new DefaultIndexOperations(mongoDbFactory, collectionName); + return new DefaultIndexOperations(mongoDbFactory, collectionName, mapper); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index 40ba7738e..488546404 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -117,6 +117,10 @@ abstract class IndexConverters { } } + if(indexOptions.containsKey("partialFilterExpression")) { + ops = ops.partialFilterExpression((org.bson.Document)indexOptions.get("partialFilterExpression")); + } + return ops; }; } @@ -124,43 +128,7 @@ abstract class IndexConverters { private static Converter getDocumentIndexInfoConverter() { return ix -> { - Document keyDocument = (Document) ix.get("key"); - int numberOfElements = keyDocument.keySet().size(); - - List indexFields = new ArrayList(numberOfElements); - - for (String key : keyDocument.keySet()) { - - Object value = keyDocument.get(key); - - if (TWO_D_IDENTIFIERS.contains(value)) { - indexFields.add(IndexField.geo(key)); - } else if ("text".equals(value)) { - - Document weights = (Document) ix.get("weights"); - for (String fieldName : weights.keySet()) { - indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString()))); - } - - } else { - - Double keyValue = new Double(value.toString()); - - if (ONE.equals(keyValue)) { - indexFields.add(IndexField.create(key, ASC)); - } else if (MINUS_ONE.equals(keyValue)) { - indexFields.add(IndexField.create(key, DESC)); - } - } - } - - String name = ix.get("name").toString(); - - boolean unique = ix.containsKey("unique") ? (Boolean) ix.get("unique") : false; - boolean sparse = ix.containsKey("sparse") ? (Boolean) ix.get("sparse") : false; - - String language = ix.containsKey("default_language") ? (String) ix.get("default_language") : ""; - return new IndexInfo(indexFields, name, unique, sparse, language); + return IndexInfo.indexInfoOf(ix); }; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexOperationsProvider.java index 50ed704eb..716f4a228 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexOperationsProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexOperationsProvider.java @@ -16,6 +16,8 @@ package org.springframework.data.mongodb.core; +import org.springframework.data.mongodb.core.convert.QueryMapper; + /** * TODO: Revisit for a better pattern. * @author Mark Paluch diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 4fd204dfc..22eda5ec2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -539,11 +539,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } public IndexOperations indexOps(String collectionName) { - return new DefaultIndexOperations(getMongoDbFactory(), collectionName); + return new DefaultIndexOperations(getMongoDbFactory(), collectionName, queryMapper); } public IndexOperations indexOps(Class entityClass) { - return new DefaultIndexOperations(getMongoDbFactory(), determineCollectionName(entityClass)); + return new DefaultIndexOperations(getMongoDbFactory(), determineCollectionName(entityClass), queryMapper); } public BulkOperations bulkOps(BulkMode bulkMode, String collectionName) { @@ -2176,7 +2176,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * * @param ids * @param documents - * @return + * @return * TODO: Remove for 2.0 and change method signature of {@link #insertDBObjectList(String, List)}. */ private static List consolidateIdentifiers(List ids, List documents) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 0e1f284ee..492cb870a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -1092,4 +1092,8 @@ public class QueryMapper { return source.getFieldName(); } } + + public MappingContext, MongoPersistentProperty> getMappingContext() { + return mappingContext; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java index 3ec6ec09b..89851f229 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java @@ -37,6 +37,7 @@ public class GeospatialIndex implements IndexDefinition { private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D; private Double bucketSize = 1.0; private String additionalField; + private IndexFilter filter; /** * Creates a new {@link GeospatialIndex} for the given field. @@ -117,6 +118,21 @@ public class GeospatialIndex implements IndexDefinition { return this; } + /** + * Only index the documents in a collection that meet a specified {@link IndexFilter filter expression}. + * + * @param filter can be {@literal null}. + * @return + * @see https://docs.mongodb.com/manual/core/index-partial/ + * @since 1.10 + */ + public GeospatialIndex partial(IndexFilter filter) { + + this.filter = filter; + return this; + } + public Document getIndexKeys() { Document document = new Document(); @@ -184,6 +200,10 @@ public class GeospatialIndex implements IndexDefinition { break; } + if (filter != null) { + dbo.put("partialFilterExpression", filter.getFilterObject()); + } + return dbo; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java index 4827ff30d..4f64e24ef 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java @@ -50,6 +50,8 @@ public class Index implements IndexDefinition { private long expire = -1; + private IndexFilter filter; + public Index() {} public Index(String key, Direction direction) { @@ -126,6 +128,21 @@ public class Index implements IndexDefinition { return this; } + /** + * Only index the documents in a collection that meet a specified {@link IndexFilter filter expression}. + * + * @param filter can be {@literal null}. + * @return + * @see https://docs.mongodb.com/manual/core/index-partial/ + * @since 1.10 + */ + public Index partial(IndexFilter filter) { + + this.filter = filter; + return this; + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.index.IndexDefinition#getIndexKeys() @@ -163,6 +180,9 @@ public class Index implements IndexDefinition { document.put("expireAfterSeconds", expire); } + if (filter != null) { + document.put("partialFilterExpression", filter.getFilterObject()); + } return document; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexFilter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexFilter.java new file mode 100644 index 000000000..46216f075 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexFilter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016. 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 + * + * http://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; + +/** + * Use {@link IndexFilter} to create the partial filter expression used when creating + * Partial Indexes. + * + * @author Christoph Strobl + * @since 1.10 + */ +public interface IndexFilter { + + /** + * Get the raw (unmapped) filter expression. + * + * @return + */ + Document getFilterObject(); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java index 5b8402970..82f27d20f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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,11 +15,16 @@ */ package org.springframework.data.mongodb.core.index; +import static org.springframework.data.domain.Sort.Direction.*; + import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import com.mongodb.DBObject; +import org.bson.Document; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -30,12 +35,17 @@ import org.springframework.util.ObjectUtils; */ public class IndexInfo { + private static final Double ONE = Double.valueOf(1); + private static final Double MINUS_ONE = Double.valueOf(-1); + private static final Collection TWO_D_IDENTIFIERS = Arrays.asList("2d", "2dsphere"); + private final List indexFields; private final String name; private final boolean unique; private final boolean sparse; private final String language; + private String partialFilterExpression; public IndexInfo(List indexFields, String name, boolean unique, boolean sparse, String language) { @@ -47,6 +57,59 @@ public class IndexInfo { this.language = language; } + /** + * Creates new {@link IndexInfo} parsing required properties from the given {@literal sourceDocument}. + * + * @param sourceDocument + * @return + * @since 1.10 + */ + public static IndexInfo indexInfoOf(Document sourceDocument) { + + Document keyDbObject = (Document) sourceDocument.get("key"); + int numberOfElements = keyDbObject.keySet().size(); + + List indexFields = new ArrayList(numberOfElements); + + for (String key : keyDbObject.keySet()) { + + Object value = keyDbObject.get(key); + + if (TWO_D_IDENTIFIERS.contains(value)) { + indexFields.add(IndexField.geo(key)); + } else if ("text".equals(value)) { + + Document weights = (Document) sourceDocument.get("weights"); + for (String fieldName : weights.keySet()) { + indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString()))); + } + + } else { + + Double keyValue = new Double(value.toString()); + + if (ONE.equals(keyValue)) { + indexFields.add(IndexField.create(key, ASC)); + } else if (MINUS_ONE.equals(keyValue)) { + indexFields.add(IndexField.create(key, DESC)); + } + } + } + + String name = sourceDocument.get("name").toString(); + + boolean unique = sourceDocument.containsKey("unique") ? (Boolean) sourceDocument.get("unique") : false; + boolean sparse = sourceDocument.containsKey("sparse") ? (Boolean) sourceDocument.get("sparse") : false; + String language = sourceDocument.containsKey("default_language") ? (String) sourceDocument.get("default_language") + : ""; + String partialFilter = sourceDocument.containsKey("partialFilterExpression") + ? ((Document)sourceDocument.get("partialFilterExpression")).toJson() : ""; + + IndexInfo info = new IndexInfo(indexFields, name, unique, sparse, language); + info.partialFilterExpression = partialFilter; + return info; + } + /** * Returns the individual index fields of the index. * @@ -94,9 +157,18 @@ public class IndexInfo { return language; } + /** + * @return + * @since 1.0 + */ + public String getPartialFilterExpression() { + return partialFilterExpression; + } + @Override public String toString() { - return "IndexInfo [indexFields=" + indexFields + ", name=" + name + ", unique=" + unique + ", sparse=" + sparse + ", language=" + language + "]"; + return "IndexInfo [indexFields=" + indexFields + ", name=" + name + ", unique=" + unique + ", sparse=" + sparse + ", language=" + language + ", partialFilterExpression=" + + partialFilterExpression + "]"; } @Override @@ -109,6 +181,7 @@ public class IndexInfo { result = prime * result + (sparse ? 1231 : 1237); result = prime * result + (unique ? 1231 : 1237); result = prime * result + ObjectUtils.nullSafeHashCode(language); + result = prime * result + ObjectUtils.nullSafeHashCode(partialFilterExpression); return result; } @@ -147,6 +220,9 @@ public class IndexInfo { if (!ObjectUtils.nullSafeEquals(language, other.language)) { return false; } + if (!ObjectUtils.nullSafeEquals(partialFilterExpression, other.partialFilterExpression)) { + return false; + } return true; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/PartialIndexFilter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/PartialIndexFilter.java new file mode 100644 index 000000000..d711b6668 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/PartialIndexFilter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016. 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 + * + * http://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.data.mongodb.core.query.CriteriaDefinition; +import org.springframework.util.Assert; + +import com.mongodb.DBObject; + +/** + * {@link IndexFilter} implementation for usage with plain {@link DBObject} as well as {@link CriteriaDefinition} filter + * expressions. + * + * @author Christoph Strobl + * @since 1.10 + */ +public class PartialIndexFilter implements IndexFilter { + + private final Object filterExpression; + + private PartialIndexFilter(Object filterExpression) { + + Assert.notNull(filterExpression, "FilterExpression must not be null!"); + this.filterExpression = filterExpression; + } + + /** + * Create new {@link PartialIndexFilter} for given {@link DBObject filter expression}. + * + * @param where must not be {@literal null}. + * @return + */ + public static PartialIndexFilter filter(Document where) { + return new PartialIndexFilter(where); + } + + /** + * Create new {@link PartialIndexFilter} for given {@link CriteriaDefinition filter expression}. + * + * @param where must not be {@literal null}. + * @return + */ + public static PartialIndexFilter filter(CriteriaDefinition where) { + return new PartialIndexFilter(where); + } + + public Document getFilterObject() { + + if (filterExpression instanceof Document) { + return (Document) filterExpression; + } + + if (filterExpression instanceof CriteriaDefinition) { + return ((CriteriaDefinition) filterExpression).getCriteriaObject(); + } + + throw new IllegalArgumentException( + String.format("Unknown type %s used as filter expression.", filterExpression.getClass())); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java index b65a2dd8a..ae821b781 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java @@ -37,6 +37,7 @@ public class TextIndexDefinition implements IndexDefinition { private Set fieldSpecs; private String defaultLanguage; private String languageOverride; + private IndexFilter filter; TextIndexDefinition() { fieldSpecs = new LinkedHashSet(); @@ -127,6 +128,10 @@ public class TextIndexDefinition implements IndexDefinition { options.put("language_override", languageOverride); } + if (filter != null) { + options.put("partialFilterExpression", filter.getFilterObject()); + } + return options; } @@ -325,6 +330,21 @@ public class TextIndexDefinition implements IndexDefinition { return this; } + /** + * Only index the documents that meet the specified {@link IndexFilter filter expression}. + * + * @param filter can be {@literal null}. + * @return + * @see https://docs.mongodb.com/manual/core/index-partial/ + * @since 1.10 + */ + public TextIndexDefinitionBuilder partial(IndexFilter filter) { + + this.instance.filter = filter; + return this; + } + public TextIndexDefinition build() { return this.instance; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java index b48e5c77d..dee0ad6d1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -17,14 +17,24 @@ package org.springframework.data.mongodb.core; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.junit.Assume.*; import static org.springframework.data.mongodb.core.ReflectiveDBCollectionInvoker.*; +import static org.springframework.data.mongodb.core.index.PartialIndexFilter.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; import org.bson.Document; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort.Direction; +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; import org.springframework.util.ObjectUtils; @@ -41,20 +51,32 @@ import com.mongodb.client.MongoCollection; @ContextConfiguration("classpath:infrastructure.xml") public class DefaultIndexOperationsIntegrationTests { - static final Document GEO_SPHERE_2D = new Document("loaction", "2dsphere"); + private static final Version THREE_DOT_TWO = new Version(3, 2); + private static Version mongoVersion; + static final org.bson.Document GEO_SPHERE_2D = new org.bson.Document("loaction", "2dsphere"); @Autowired MongoTemplate template; DefaultIndexOperations indexOps; - MongoCollection collection; + MongoCollection collection; @Before public void setUp() { + queryMongoVersionIfNecessary(); String collectionName = this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class); this.collection = this.template.getDb().getCollection(collectionName, Document.class); this.collection.dropIndexes(); - this.indexOps = new DefaultIndexOperations(template.getMongoDbFactory(), collectionName); + this.indexOps = new DefaultIndexOperations(template.getMongoDbFactory(), collectionName, + new QueryMapper(template.getConverter())); + } + + private void queryMongoVersionIfNecessary() { + + if (mongoVersion == null) { + Document result = template.executeCommand("{ buildInfo: 1 }"); + mongoVersion = Version.parse(result.get("version").toString()); + } } /** @@ -69,11 +91,83 @@ public class DefaultIndexOperationsIntegrationTests { assertThat(info.getIndexFields().get(0).isGeo(), is(true)); } - private IndexInfo findAndReturnIndexInfo(Document keys) { + /** + * @see DATAMONGO-1467 + */ + @Test + public void shouldApplyPartialFilterCorrectly() { + + assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true)); + + IndexDefinition id = new Index().named("partial-with-criteria").on("k3y", Direction.ASC) + .partial(filter(where("q-t-y").gte(10))); + + indexOps.ensureIndex(id); + + IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-criteria"); + assertThat(info.getPartialFilterExpression(), is(equalTo("{ \"q-t-y\" : { \"$gte\" : 10 } }"))); + } + + /** + * @see DATAMONGO-1467 + */ + @Test + 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(filter(where("quantity").gte(10))); + + indexOps.ensureIndex(id); + + IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-mapped-criteria"); + assertThat(info.getPartialFilterExpression(), is(equalTo("{ \"qty\" : { \"$gte\" : 10 } }"))); + } + + /** + * @see DATAMONGO-1467 + */ + @Test + public void shouldApplyPartialDBOFilterCorrectly() { + + assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true)); + + IndexDefinition id = new Index().named("partial-with-dbo").on("k3y", Direction.ASC) + .partial(filter(new org.bson.Document("qty", new org.bson.Document("$gte", 10)))); + + indexOps.ensureIndex(id); + + IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-dbo"); + assertThat(info.getPartialFilterExpression(), is(equalTo("{ \"qty\" : { \"$gte\" : 10 } }"))); + } + + /** + * @see DATAMONGO-1467 + */ + @Test + public void shouldFavorExplicitMappingHintViaClass() { + + assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO), is(true)); + + IndexDefinition id = new Index().named("partial-with-inheritance").on("k3y", Direction.ASC) + .partial(filter(where("age").gte(10))); + + indexOps = new DefaultIndexOperations(template.getMongoDbFactory(), + this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class), + new QueryMapper(template.getConverter()), MappingToSameCollection.class); + + indexOps.ensureIndex(id); + + IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-inheritance"); + assertThat(info.getPartialFilterExpression(), is(equalTo("{ \"a_g_e\" : { \"$gte\" : 10 } }"))); + } + + private IndexInfo findAndReturnIndexInfo(org.bson.Document keys) { return findAndReturnIndexInfo(indexOps.getIndexInfo(), keys); } - private static IndexInfo findAndReturnIndexInfo(Iterable candidates, Document keys) { + private static IndexInfo findAndReturnIndexInfo(Iterable candidates, org.bson.Document keys) { return findAndReturnIndexInfo(candidates, generateIndexName(keys)); } @@ -87,5 +181,15 @@ public class DefaultIndexOperationsIntegrationTests { throw new AssertionError(String.format("Index with %s was not found", name)); } - static class DefaultIndexOperationsIntegrationTestsSample {} + @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 DefaultIndexOperationsIntegrationTestsSample { + + @Field("a_g_e") Integer age; + } }