From d09e14f6777c4a46d57df77e89b29facccea61ea Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 24 Oct 2016 19:00:06 +0200 Subject: [PATCH 1/2] DATAMONGO-1467 - Support partial filter expressions for indexing. Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-cross-store/pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb-log4j/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index ed9720988d..f99c3a9d4f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1467-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index ae0a5d6c8f..df39b42bad 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1467-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1467-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 2d02722262..3b9d07cc27 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1467-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index ee5e3336db..204ca20f4d 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1467-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 8072d3f665..e06b108fd9 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1467-SNAPSHOT ../pom.xml From 5b2b5520de6f1982157d74001aec2e5d2aa49509 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 19 Dec 2016 08:24:05 +0100 Subject: [PATCH 2/2] DATAMONGO-1467 - Add support for MongoDB 3.2 partialFilterExpression for index creation. We now support `partialFilterExpression` via `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. --- .../mongodb/core/DefaultIndexOperations.java | 97 ++++++++-------- .../data/mongodb/core/MongoTemplate.java | 2 +- .../mongodb/core/index/GeospatialIndex.java | 23 +++- .../data/mongodb/core/index/Index.java | 22 +++- .../data/mongodb/core/index/IndexFilter.java | 36 ++++++ .../data/mongodb/core/index/IndexInfo.java | 81 ++++++++++++- .../core/index/PartialIndexFilter.java | 73 ++++++++++++ .../core/index/TextIndexDefinition.java | 32 +++++- ...efaultIndexOperationsIntegrationTests.java | 108 +++++++++++++++++- 9 files changed, 414 insertions(+), 60 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/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java index a1f2c96725..a019ed036a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2011-2015 the original author or authors. + * Copyright 2011-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,17 +15,15 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.domain.Sort.Direction.*; - import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; import org.springframework.dao.DataAccessException; +import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.index.IndexDefinition; -import org.springframework.data.mongodb.core.index.IndexField; import org.springframework.data.mongodb.core.index.IndexInfo; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.util.Assert; import com.mongodb.DBCollection; @@ -42,12 +40,11 @@ */ public class DefaultIndexOperations implements IndexOperations { - 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"); - + public static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression"; private final MongoOperations mongoOperations; private final String collectionName; + private final QueryMapper mapper; + private final Class type; /** * Creates a new {@link DefaultIndexOperations}. @@ -56,12 +53,26 @@ public class DefaultIndexOperations implements IndexOperations { * @param collectionName must not be {@literal null}. */ public DefaultIndexOperations(MongoOperations mongoOperations, String collectionName) { + this(mongoOperations, collectionName, null); + } + + /** + * Creates a new {@link DefaultIndexOperations}. + * + * @param mongoOperations must not be {@literal null}. + * @param collectionName 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(MongoOperations mongoOperations, String collectionName, Class type) { Assert.notNull(mongoOperations, "MongoOperations must not be null!"); Assert.notNull(collectionName, "Collection name can not be null!"); this.mongoOperations = mongoOperations; this.collectionName = collectionName; + this.mapper = new QueryMapper(mongoOperations.getConverter()); + this.type = type; } /* @@ -69,9 +80,20 @@ public DefaultIndexOperations(MongoOperations mongoOperations, String collection * @see org.springframework.data.mongodb.core.IndexOperations#ensureIndex(org.springframework.data.mongodb.core.index.IndexDefinition) */ public void ensureIndex(final IndexDefinition indexDefinition) { + mongoOperations.execute(collectionName, new CollectionCallback() { public Object doInCollection(DBCollection collection) throws MongoException, DataAccessException { DBObject indexOptions = indexDefinition.getIndexOptions(); + + if (indexOptions != null && indexOptions.containsField(PARTIAL_FILTER_EXPRESSION_KEY)) { + + Assert.isInstanceOf(DBObject.class, indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY)); + + indexOptions.put(PARTIAL_FILTER_EXPRESSION_KEY, + mapper.getMappedObject((DBObject) indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), + lookupPersistentEntity(type, collectionName))); + } + if (indexOptions != null) { collection.createIndex(indexDefinition.getIndexKeys(), indexOptions); } else { @@ -79,6 +101,24 @@ public Object doInCollection(DBCollection collection) throws MongoException, Dat } return null; } + + private MongoPersistentEntity lookupPersistentEntity(Class entityType, String collection) { + + if (entityType != null) { + return mongoOperations.getConverter().getMappingContext().getPersistentEntity(entityType); + } + + Collection> entities = mongoOperations.getConverter().getMappingContext() + .getPersistentEntities(); + + for (MongoPersistentEntity entity : entities) { + if (entity.getCollection().equals(collection)) { + return entity; + } + } + + return null; + } }); } @@ -136,44 +176,7 @@ private List getIndexData(List dbObjectList) { List indexInfoList = new ArrayList(); for (DBObject ix : dbObjectList) { - - DBObject keyDbObject = (DBObject) ix.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)) { - - DBObject weights = (DBObject) 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.containsField("unique") ? (Boolean) ix.get("unique") : false; - boolean dropDuplicates = ix.containsField("dropDups") ? (Boolean) ix.get("dropDups") : false; - boolean sparse = ix.containsField("sparse") ? (Boolean) ix.get("sparse") : false; - String language = ix.containsField("default_language") ? (String) ix.get("default_language") : ""; - indexInfoList.add(new IndexInfo(indexFields, name, unique, dropDuplicates, sparse, language)); + indexInfoList.add(IndexInfo.indexInfoOf(ix)); } return indexInfoList; 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 a126edb269..242563b05e 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 @@ -557,7 +557,7 @@ public IndexOperations indexOps(String collectionName) { } public IndexOperations indexOps(Class entityClass) { - return new DefaultIndexOperations(this, determineCollectionName(entityClass)); + return new DefaultIndexOperations(this, determineCollectionName(entityClass), entityClass); } public BulkOperations bulkOps(BulkMode bulkMode, String collectionName) { 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 64d8841a66..e7d585f868 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 @@ -1,5 +1,5 @@ /* - * Copyright 2010-2014 the original author or authors. + * Copyright 2010-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. @@ -39,6 +39,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. @@ -119,6 +120,22 @@ public GeospatialIndex withAdditionalField(String fieldName) { 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 DBObject getIndexKeys() { DBObject dbo = new BasicDBObject(); @@ -186,6 +203,10 @@ public DBObject getIndexOptions() { 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 dbf59f6e2b..29e84f716a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2010-2015 the original author or authors. + * Copyright 2010-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. @@ -63,6 +63,8 @@ public enum Duplicates { private long expire = -1; + private IndexFilter filter; + public Index() {} public Index(String key, Direction direction) { @@ -176,6 +178,21 @@ public Index unique(Duplicates duplicates) { return unique(); } + /** + * 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() @@ -213,6 +230,9 @@ public DBObject getIndexOptions() { dbo.put("expireAfterSeconds", expire); } + 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/IndexFilter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexFilter.java new file mode 100644 index 0000000000..b2e73627ec --- /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 com.mongodb.DBObject; + +/** + * 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 + */ + DBObject 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 2073fc1c28..2fc1e840da 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,15 @@ */ 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.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -30,6 +34,10 @@ */ 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; @@ -37,6 +45,7 @@ public class IndexInfo { private final boolean dropDuplicates; private final boolean sparse; private final String language; + private String partialFilterExpression; /** * @deprecated Will be removed in 1.7. Please use {@link #IndexInfo(List, String, boolean, boolean, boolean, String)} @@ -62,6 +71,61 @@ public IndexInfo(List indexFields, String name, boolean unique, bool 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(DBObject sourceDocument) { + + DBObject keyDbObject = (DBObject) 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)) { + + DBObject weights = (DBObject) 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.containsField("unique") ? (Boolean) sourceDocument.get("unique") : false; + boolean dropDuplicates = sourceDocument.containsField("dropDups") ? (Boolean) sourceDocument.get("dropDups") + : false; + boolean sparse = sourceDocument.containsField("sparse") ? (Boolean) sourceDocument.get("sparse") : false; + String language = sourceDocument.containsField("default_language") ? (String) sourceDocument.get("default_language") + : ""; + String partialFilter = sourceDocument.containsField("partialFilterExpression") + ? sourceDocument.get("partialFilterExpression").toString() : ""; + + IndexInfo info = new IndexInfo(indexFields, name, unique, dropDuplicates, sparse, language); + info.partialFilterExpression = partialFilter; + return info; + } + /** * Returns the individual index fields of the index. * @@ -113,10 +177,19 @@ public String getLanguage() { return language; } + /** + * @return + * @since 1.0 + */ + public String getPartialFilterExpression() { + return partialFilterExpression; + } + @Override public String toString() { return "IndexInfo [indexFields=" + indexFields + ", name=" + name + ", unique=" + unique + ", dropDuplicates=" - + dropDuplicates + ", sparse=" + sparse + ", language=" + language + "]"; + + dropDuplicates + ", sparse=" + sparse + ", language=" + language + ", partialFilterExpression=" + + partialFilterExpression + "]"; } @Override @@ -130,6 +203,7 @@ public int hashCode() { 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; } @@ -171,6 +245,9 @@ public boolean equals(Object obj) { 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 0000000000..d4a1ed2679 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/PartialIndexFilter.java @@ -0,0 +1,73 @@ +/* + * 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.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(DBObject 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 DBObject getFilterObject() { + + if (filterExpression instanceof DBObject) { + return (DBObject) 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 9fb64be3fb..8e56da94f6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014 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. @@ -39,6 +39,7 @@ public class TextIndexDefinition implements IndexDefinition { private Set fieldSpecs; private String defaultLanguage; private String languageOverride; + private IndexFilter filter; TextIndexDefinition() { fieldSpecs = new LinkedHashSet(); @@ -129,6 +130,10 @@ public DBObject getIndexOptions() { options.put("language_override", languageOverride); } + if (filter != null) { + options.put("partialFilterExpression", filter.getFilterObject()); + } + return options; } @@ -288,8 +293,8 @@ public TextIndexDefinitionBuilder onField(String fieldname) { public TextIndexDefinitionBuilder onField(String fieldname, Float weight) { if (this.instance.fieldSpecs.contains(ALL_FIELDS)) { - throw new InvalidDataAccessApiUsageException(String.format("Cannot add %s to field spec for all fields.", - fieldname)); + throw new InvalidDataAccessApiUsageException( + String.format("Cannot add %s to field spec for all fields.", fieldname)); } this.instance.fieldSpecs.add(new TextIndexedFieldSpec(fieldname, weight)); @@ -318,15 +323,30 @@ public TextIndexDefinitionBuilder withDefaultLanguage(String language) { public TextIndexDefinitionBuilder withLanguageOverride(String fieldname) { if (StringUtils.hasText(this.instance.languageOverride)) { - throw new InvalidDataAccessApiUsageException(String.format( - "Cannot set language override on %s as it is already defined on %s.", fieldname, - this.instance.languageOverride)); + throw new InvalidDataAccessApiUsageException( + String.format("Cannot set language override on %s as it is already defined on %s.", fieldname, + this.instance.languageOverride)); } this.instance.languageOverride = fieldname; 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 744387de80..03e10d092f 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,18 +17,28 @@ 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.junit.Before; 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.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.Document; +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; import com.mongodb.BasicDBObject; +import com.mongodb.CommandResult; import com.mongodb.DBCollection; import com.mongodb.DBObject; @@ -42,6 +52,9 @@ @ContextConfiguration("classpath:infrastructure.xml") public class DefaultIndexOperationsIntegrationTests { + private static final Version THREE_DOT_TWO = new Version(3, 2); + private static Version mongoVersion; + static final DBObject GEO_SPHERE_2D = new BasicDBObject("loaction", "2dsphere"); @Autowired MongoTemplate template; @@ -51,6 +64,7 @@ public class DefaultIndexOperationsIntegrationTests { @Before public void setUp() { + queryMongoVersionIfNecessary(); String collectionName = this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class); this.collection = this.template.getDb().getCollection(collectionName); @@ -59,6 +73,14 @@ public void setUp() { this.indexOps = new DefaultIndexOperations(template, collectionName); } + private void queryMongoVersionIfNecessary() { + + if (mongoVersion == null) { + CommandResult result = template.executeCommand("{ buildInfo: 1 }"); + mongoVersion = Version.parse(result.get("version").toString()); + } + } + /** * @see DATAMONGO-1008 */ @@ -71,6 +93,78 @@ public void getIndexInfoShouldBeAbleToRead2dsphereIndex() { assertThat(info.getIndexFields().get(0).isGeo(), is(true)); } + /** + * @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 BasicDBObject("qty", new BasicDBObject("$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, + this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class), + 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(DBObject keys) { return findAndReturnIndexInfo(indexOps.getIndexInfo(), keys); } @@ -89,5 +183,15 @@ private static IndexInfo findAndReturnIndexInfo(Iterable candidates, throw new AssertionError(String.format("Index with %s was not found", name)); } - static class DefaultIndexOperationsIntegrationTestsSample {} + @Document(collection = "default-index-operations-tests") + static class DefaultIndexOperationsIntegrationTestsSample { + + @Field("qty") Integer quantity; + } + + @Document(collection = "default-index-operations-tests") + static class MappingToSameCollection extends DefaultIndexOperationsIntegrationTestsSample { + + @Field("a_g_e") Integer age; + } }