From 7848da63f2da66d643e863d17921ae0abc81a73a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 10 Apr 2014 19:51:41 +0200 Subject: [PATCH] DATAMONGO-899 - Ensure proper creation of index structures for nested entities. Index creation did not consider the properties path when creating the index. This lead to broken index creation when nesting entities that might require index structures. Off now index creation traverses the entities property path for all top level entities (namely those holding the @Document annotation) and creates the index using the full property path. This is a breaking change as having an entity to carry the @Document annotation has not been required by now. Original Pull Request: #168 --- .../core/index/CompoundIndexDefinition.java | 42 ++ .../mongodb/core/index/GeospatialIndex.java | 21 +- .../data/mongodb/core/index/Index.java | 77 +++- .../mongodb/core/index/IndexDefinition.java | 10 +- .../mongodb/core/index/IndexResolver.java | 35 ++ .../MongoPersistentEntityIndexCreator.java | 147 ++----- .../MongoPersistentEntityIndexResolver.java | 351 ++++++++++++++++ .../core/geo/GeoSpatialIndexTests.java | 5 + .../core/index/IndexingIntegrationTests.java | 15 +- ...PersistentEntityIndexCreatorUnitTests.java | 124 ++++-- ...ersistentEntityIndexResolverUnitTests.java | 390 ++++++++++++++++++ src/docbkx/reference/mapping.xml | 5 + 12 files changed, 1066 insertions(+), 156 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndexDefinition.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexResolver.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndexDefinition.java new file mode 100644 index 000000000..890d3cf64 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndexDefinition.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014 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.BasicDBObject; +import com.mongodb.DBObject; + +/** + * @author Christoph Strobl + * @since 1.5 + */ +public class CompoundIndexDefinition extends Index { + + private DBObject keys; + + public CompoundIndexDefinition(DBObject keys) { + this.keys = keys; + } + + @Override + public DBObject getIndexKeys() { + + BasicDBObject dbo = new BasicDBObject(); + dbo.putAll(this.keys); + dbo.putAll(super.getIndexKeys()); + return dbo; + } + +} 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 8ddc70885..3a14f6551 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-2013 the original author or authors. + * Copyright 2010-2014 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. @@ -27,6 +27,7 @@ import com.mongodb.DBObject; * @author Jon Brisbin * @author Oliver Gierke * @author Laurent Canet + * @author Christoph Strobl */ public class GeospatialIndex implements IndexDefinition { @@ -38,6 +39,7 @@ public class GeospatialIndex implements IndexDefinition { private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D; private Double bucketSize = 1.0; private String additionalField; + private String collection; /** * Creates a new {@link GeospatialIndex} for the given field. @@ -120,6 +122,23 @@ public class GeospatialIndex implements IndexDefinition { return this; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.index.IndexDefinition#getCollection() + */ + @Override + public String getCollection() { + return collection; + } + + /** + * @param collection + * @since 1.5 + */ + public void setCollection(String collection) { + this.collection = collection; + } + public DBObject getIndexKeys() { DBObject dbo = new BasicDBObject(); 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 c7bbd7c8c..b12fed883 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-2013 the original author or authors. + * Copyright 2010-2014 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,13 +17,19 @@ package org.springframework.data.mongodb.core.index; import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.query.Order; +import org.springframework.util.Assert; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; +/** + * @author Oliver Gierke + * @author Christoph Strobl + */ @SuppressWarnings("deprecation") public class Index implements IndexDefinition { @@ -41,6 +47,12 @@ public class Index implements IndexDefinition { private boolean sparse = false; + private boolean background = false; + + private long expire = -1; + + private String collection; + public Index() {} public Index(String key, Direction direction) { @@ -104,6 +116,61 @@ public class Index implements IndexDefinition { return this; } + /** + * Build the index in background (non blocking). + * + * @return + * @since 1.5 + */ + public Index background() { + + this.background = true; + return this; + } + + /** + * Specifies TTL in seconds. + * + * @param value + * @return + * @since 1.5 + */ + public Index expire(long value) { + return expire(value, TimeUnit.SECONDS); + } + + /** + * Specifies TTL with given {@link TimeUnit}. + * + * @param value + * @param unit + * @return + * @since 1.5 + */ + public Index expire(long value, TimeUnit unit) { + + Assert.notNull(unit, "TimeUnit for expiration must not be null."); + this.expire = unit.toSeconds(value); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.index.IndexDefinition#getCollection() + */ + @Override + public String getCollection() { + return collection; + } + + /** + * @param collection + * @since 1.5 + */ + public void setCollection(String collection) { + this.collection = collection; + } + /** * @see http://docs.mongodb.org/manual/core/index-creation/#index-creation-duplicate-dropping * @param duplicates @@ -125,9 +192,11 @@ public class Index implements IndexDefinition { } public DBObject getIndexOptions() { + if (name == null && !unique) { return null; } + DBObject dbo = new BasicDBObject(); if (name != null) { dbo.put("name", name); @@ -141,6 +210,12 @@ public class Index implements IndexDefinition { if (sparse) { dbo.put("sparse", true); } + if (background) { + dbo.put("background", true); + } + if (expire >= 0) { + dbo.put("expireAfterSeconds", expire); + } return dbo; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexDefinition.java index e6317e9ae..ea28c3b68 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 by the original author(s). + * Copyright (c) 2011-2014 by the original author(s). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import com.mongodb.DBObject; /** * @author Jon Brisbin + * @author Christoph Strobl */ public interface IndexDefinition { @@ -27,4 +28,11 @@ public interface IndexDefinition { DBObject getIndexOptions(); + /** + * Get the collection name for the index. + * + * @return + * @since 1.5 + */ + String getCollection(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexResolver.java new file mode 100644 index 000000000..2f829fe54 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexResolver.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 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; + +/** + * {@link IndexResolver} finds those {@link IndexDefinition}s to be created for a given class. + * + * @author Christoph Strobl + * @since 1.5 + */ +public interface IndexResolver { + + /** + * Find and create {@link IndexDefinition}s for properties of given {@code type}. {@link IndexDefinition}s are created + * for properties and types with {@link Indexed}, {@link CompoundIndexes} or {@link GeoSpatialIndexed}. + * + * @param type + * @return Empty {@link Iterable} in case no {@link IndexDefinition} could be resolved for type. + */ + Iterable resolveIndexForClass(Class type); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java index 02ad1c50c..d0760eb0c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java @@ -22,19 +22,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContextEvent; import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; -import com.mongodb.util.JSON; /** * Component that inspects {@link MongoPersistentEntity} instances contained in the given {@link MongoMappingContext} @@ -45,6 +40,7 @@ import com.mongodb.util.JSON; * @author Philipp Schneider * @author Johno Crawford * @author Laurent Canet + * @author Christoph Strobl */ public class MongoPersistentEntityIndexCreator implements ApplicationListener, MongoPersistentProperty>> { @@ -54,21 +50,37 @@ public class MongoPersistentEntityIndexCreator implements private final Map, Boolean> classesSeen = new ConcurrentHashMap, Boolean>(); private final MongoDbFactory mongoDbFactory; private final MongoMappingContext mappingContext; + private final IndexResolver indexResolver; /** * Creats a new {@link MongoPersistentEntityIndexCreator} for the given {@link MongoMappingContext} and * {@link MongoDbFactory}. * - * @param mappingContext must not be {@literal null} - * @param mongoDbFactory must not be {@literal null} + * @param mappingContext must not be {@literal null}. + * @param mongoDbFactory must not be {@literal null}. */ public MongoPersistentEntityIndexCreator(MongoMappingContext mappingContext, MongoDbFactory mongoDbFactory) { + this(mappingContext, mongoDbFactory, new MongoPersistentEntityIndexResolver(mappingContext)); + } + + /** + * Creats a new {@link MongoPersistentEntityIndexCreator} for the given {@link MongoMappingContext} and + * {@link MongoDbFactory}. + * + * @param mappingContext must not be {@literal null}. + * @param mongoDbFactory must not be {@literal null}. + * @param indexResolver must not be {@literal null}. + */ + public MongoPersistentEntityIndexCreator(MongoMappingContext mappingContext, MongoDbFactory mongoDbFactory, + IndexResolver indexResolver) { Assert.notNull(mongoDbFactory); Assert.notNull(mappingContext); + Assert.notNull(indexResolver); this.mongoDbFactory = mongoDbFactory; this.mappingContext = mappingContext; + this.indexResolver = indexResolver; for (MongoPersistentEntity entity : mappingContext.getPersistentEntities()) { checkForIndexes(entity); @@ -93,87 +105,34 @@ public class MongoPersistentEntityIndexCreator implements } } - protected void checkForIndexes(final MongoPersistentEntity entity) { + private void checkForIndexes(final MongoPersistentEntity entity) { final Class type = entity.getType(); if (!classesSeen.containsKey(type)) { + + this.classesSeen.put(type, Boolean.TRUE); + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Analyzing class " + type + " for index information."); } - // Make sure indexes get created - if (type.isAnnotationPresent(CompoundIndexes.class)) { - CompoundIndexes indexes = type.getAnnotation(CompoundIndexes.class); - for (CompoundIndex index : indexes.value()) { - - String indexColl = StringUtils.hasText(index.collection()) ? index.collection() : entity.getCollection(); - DBObject definition = (DBObject) JSON.parse(index.def()); - - ensureIndex(indexColl, index.name(), definition, index.unique(), index.dropDups(), index.sparse(), - index.background(), index.expireAfterSeconds()); - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Created compound index " + index); - } - } - } - - entity.doWithProperties(new PropertyHandler() { - public void doWithPersistentProperty(MongoPersistentProperty property) { - - if (property.isAnnotationPresent(Indexed.class)) { - - Indexed index = property.findAnnotation(Indexed.class); - String name = index.name(); - - if (!StringUtils.hasText(name)) { - name = property.getFieldName(); - } else { - if (!name.equals(property.getName()) && index.unique() && !index.sparse()) { - // Names don't match, and sparse is not true. This situation will generate an error on the server. - if (LOGGER.isWarnEnabled()) { - LOGGER.warn("The index name " + name + " doesn't match this property name: " + property.getName() - + ". Setting sparse=true on this index will prevent errors when inserting documents."); - } - } - } - - String collection = StringUtils.hasText(index.collection()) ? index.collection() : entity.getCollection(); - int direction = index.direction() == IndexDirection.ASCENDING ? 1 : -1; - DBObject definition = new BasicDBObject(property.getFieldName(), direction); - - ensureIndex(collection, name, definition, index.unique(), index.dropDups(), index.sparse(), - index.background(), index.expireAfterSeconds()); - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Created property index " + index); - } - - } else if (property.isAnnotationPresent(GeoSpatialIndexed.class)) { - - GeoSpatialIndexed index = property.findAnnotation(GeoSpatialIndexed.class); - - GeospatialIndex indexObject = new GeospatialIndex(property.getFieldName()); - indexObject.withMin(index.min()).withMax(index.max()); - indexObject.named(StringUtils.hasText(index.name()) ? index.name() : property.getName()); - indexObject.typed(index.type()).withBucketSize(index.bucketSize()) - .withAdditionalField(index.additionalField()); - - String collection = StringUtils.hasText(index.collection()) ? index.collection() : entity.getCollection(); - mongoDbFactory.getDb().getCollection(collection) - .ensureIndex(indexObject.getIndexKeys(), indexObject.getIndexOptions()); - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("Created %s for entity %s in collection %s! ", indexObject, entity.getType(), - collection)); - } - } - } - }); - - classesSeen.put(type, true); + checkForAndCreateIndexes(entity); } } + protected void checkForAndCreateIndexes(MongoPersistentEntity entity) { + + if (entity.findAnnotation(Document.class) != null) { + for (IndexDefinition indexToCreate : indexResolver.resolveIndexForClass(entity.getType())) { + createIndex(indexToCreate); + } + } + } + + protected void createIndex(IndexDefinition indexDefinition) { + mongoDbFactory.getDb().getCollection(indexDefinition.getCollection()) + .ensureIndex(indexDefinition.getIndexKeys(), indexDefinition.getIndexOptions()); + } + /** * Returns whether the current index creator was registered for the given {@link MappingContext}. * @@ -184,32 +143,4 @@ public class MongoPersistentEntityIndexCreator implements return this.mappingContext.equals(context); } - /** - * Triggers the actual index creation. - * - * @param collection the collection to create the index in - * @param name the name of the index about to be created - * @param indexDefinition the index definition - * @param unique whether it shall be a unique index - * @param dropDups whether to drop duplicates - * @param sparse sparse or not - * @param background whether the index will be created in the background - * @param expireAfterSeconds the time to live for documents in the collection - */ - protected void ensureIndex(String collection, String name, DBObject indexDefinition, boolean unique, - boolean dropDups, boolean sparse, boolean background, int expireAfterSeconds) { - - DBObject opts = new BasicDBObject(); - opts.put("name", name); - opts.put("dropDups", dropDups); - opts.put("sparse", sparse); - opts.put("unique", unique); - opts.put("background", background); - - if (expireAfterSeconds != -1) { - opts.put("expireAfterSeconds", expireAfterSeconds); - } - - mongoDbFactory.getDb().getCollection(collection).ensureIndex(indexDefinition, opts); - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java new file mode 100644 index 000000000..78a94b294 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -0,0 +1,351 @@ +/* + * Copyright 2014 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mongodb.core.index.Index.Duplicates; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.mongodb.DBObject; +import com.mongodb.util.JSON; + +/** + * {@link IndexResolver} implementation inspecting {@link MongoPersistentEntity} for {@link MongoPersistentEntity} to be + * indexed.
+ * All {@link MongoPersistentProperty} of the {@link MongoPersistentEntity} are inspected for potential indexes by + * scanning related annotations. + * + * @author Christoph Strobl + * @since 1.5 + */ +public class MongoPersistentEntityIndexResolver implements IndexResolver { + + private final MongoMappingContext mappingContext; + + /** + * Create new {@link MongoPersistentEntityIndexResolver}. + * + * @param mappingContext must not be {@literal null}. + */ + public MongoPersistentEntityIndexResolver(MongoMappingContext mappingContext) { + + Assert.notNull(mappingContext, "Mapping context must not be null in order to resolve index definitions"); + this.mappingContext = mappingContext; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.index.IndexResolver#resolveIndexForClass(java.lang.Class) + */ + @Override + public List resolveIndexForClass(Class type) { + return resolveIndexForEntity(mappingContext.getPersistentEntity(type)); + } + + /** + * Resolve the {@link IndexDefinition}s for given {@literal root} entity by traversing {@link MongoPersistentProperty} + * scanning for index annotations {@link Indexed}, {@link CompoundIndex} and {@link GeospatialIndex}. The given + * {@literal root} has therefore to be annotated with {@link Document}. + * + * @param root must not be null. + * @return List of {@link IndexDefinitionHolder}. Will never be {@code null}. + * @throws IllegalArgumentException in case of missing {@link Document} annotation marking root entities. + */ + public List resolveIndexForEntity(final MongoPersistentEntity root) { + + Assert.notNull(root, "Index cannot be resolved for given 'null' entity."); + Document document = root.findAnnotation(Document.class); + Assert.notNull(document, "Given entity is not collection root."); + + final List indexInformation = new ArrayList(); + indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", root.getCollection(), root.getType())); + + root.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) { + + if (persistentProperty.isEntity()) { + indexInformation.addAll(resolveIndexForClass(persistentProperty.getActualType(), + persistentProperty.getFieldName(), root.getCollection())); + } + + IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty( + persistentProperty.getFieldName(), root.getCollection(), persistentProperty); + if (indexDefinitionHolder != null) { + indexInformation.add(indexDefinitionHolder); + } + } + }); + + return indexInformation; + } + + /** + * Recursively resolve and inspect properties of given {@literal type} for indexes to be created. + * + * @param type + * @param path The {@literal "dot} path. + * @param collection + * @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property + * types. Will never be {@code null}. + */ + private List resolveIndexForClass(Class type, final String path, final String collection) { + + final List indexInformation = new ArrayList(); + indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(path, collection, type)); + + MongoPersistentEntity entity = mappingContext.getPersistentEntity(type); + entity.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) { + + String propertyDotPath = (StringUtils.hasText(path) ? (path + ".") : "") + persistentProperty.getFieldName(); + + if (persistentProperty.isEntity()) { + indexInformation.addAll(resolveIndexForClass(persistentProperty.getActualType(), propertyDotPath, collection)); + } + + IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty(propertyDotPath, + collection, persistentProperty); + if (indexDefinitionHolder != null) { + indexInformation.add(indexDefinitionHolder); + } + } + }); + + return indexInformation; + } + + private IndexDefinitionHolder createIndexDefinitionHolderForProperty(String dotPath, String collection, + MongoPersistentProperty persistentProperty) { + + if (persistentProperty.isAnnotationPresent(Indexed.class)) { + return createIndexDefinition(dotPath, collection, persistentProperty); + } else if (persistentProperty.isAnnotationPresent(GeoSpatialIndexed.class)) { + return createGeoSpatialIndexDefinition(dotPath, collection, persistentProperty); + } + + return null; + } + + private List potentiallyCreateCompoundIndexDefinitions(String dotPath, String collection, + Class type) { + + if (AnnotationUtils.findAnnotation(type, CompoundIndexes.class) == null) { + return Collections.emptyList(); + } + + return createCompoundIndexDefinitions(dotPath, collection, type); + } + + /** + * Create {@link IndexDefinition} wrapped in {@link IndexDefinitionHolder} for {@link CompoundIndexes} of given type. + * + * @param dotPath The properties {@literal "dot"} path representation from its document root. + * @param collection + * @param type + * @return + */ + protected List createCompoundIndexDefinitions(String dotPath, String collection, Class type) { + + CompoundIndexes indexes = AnnotationUtils.findAnnotation(type, CompoundIndexes.class); + List indexDefinitions = new ArrayList( + indexes.value().length); + + for (CompoundIndex index : indexes.value()) { + + IndexDefinitionHolder holder = new IndexDefinitionHolder(StringUtils.hasText(index.name()) ? index.name() + : dotPath); + + CompoundIndexDefinition indexDefinition = new CompoundIndexDefinition((DBObject) JSON.parse(index.def())); + indexDefinition.named(index.name()); + indexDefinition.setCollection(StringUtils.hasText(index.collection()) ? index.collection() : collection); + if (index.unique()) { + indexDefinition.unique(index.dropDups() ? Duplicates.DROP : Duplicates.RETAIN); + } + if (index.sparse()) { + indexDefinition.sparse(); + } + if (index.background()) { + indexDefinition.background(); + } + if (index.expireAfterSeconds() >= 0) { + indexDefinition.expire(index.expireAfterSeconds(), TimeUnit.SECONDS); + } + + holder.setIndexDefinition(indexDefinition); + indexDefinitions.add(holder); + } + + return indexDefinitions; + } + + /** + * Creates {@link IndexDefinition} wrapped in {@link IndexDefinitionHolder} out of {@link Indexed} for given + * {@link MongoPersistentProperty}. + * + * @param dotPath The properties {@literal "dot"} path representation from its document root. + * @param collection + * @param persitentProperty + * @return + */ + protected IndexDefinitionHolder createIndexDefinition(String dotPath, String collection, + MongoPersistentProperty persitentProperty) { + + Indexed index = persitentProperty.findAnnotation(Indexed.class); + + IndexDefinitionHolder holder = new IndexDefinitionHolder(dotPath); + + Index indexDefinition = new Index(); + indexDefinition.setCollection(StringUtils.hasText(index.collection()) ? index.collection() : collection); + indexDefinition.named(StringUtils.hasText(index.name()) ? index.name() : persitentProperty.getFieldName()); + indexDefinition.on(persitentProperty.getFieldName(), + IndexDirection.ASCENDING.equals(index.direction()) ? Sort.Direction.ASC : Sort.Direction.DESC); + + if (index.unique()) { + indexDefinition.unique(index.dropDups() ? Duplicates.DROP : Duplicates.RETAIN); + } + if (index.sparse()) { + indexDefinition.sparse(); + } + if (index.background()) { + indexDefinition.background(); + } + if (index.expireAfterSeconds() >= 0) { + indexDefinition.expire(index.expireAfterSeconds(), TimeUnit.SECONDS); + } + + holder.setIndexDefinition(indexDefinition); + return holder; + } + + /** + * Creates {@link IndexDefinition} wrapped in {@link IndexDefinitionHolder} out of {@link GeoSpatialIndexed} for + * {@link MongoPersistentProperty}. + * + * @param dotPath The properties {@literal "dot"} path representation from its document root. + * @param collection + * @param persistentProperty + * @return + */ + protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, String collection, + MongoPersistentProperty persistentProperty) { + + GeoSpatialIndexed index = persistentProperty.findAnnotation(GeoSpatialIndexed.class); + + IndexDefinitionHolder holder = new IndexDefinitionHolder(dotPath); + + GeospatialIndex indexDefinition = new GeospatialIndex(dotPath); + indexDefinition.setCollection(StringUtils.hasText(index.collection()) ? index.collection() : collection); + indexDefinition.withBits(index.bits()); + indexDefinition.withMin(index.min()).withMax(index.max()); + indexDefinition.named(StringUtils.hasText(index.name()) ? index.name() : persistentProperty.getName()); + indexDefinition.typed(index.type()).withBucketSize(index.bucketSize()).withAdditionalField(index.additionalField()); + + holder.setIndexDefinition(indexDefinition); + return holder; + } + + /** + * Implementation of {@link IndexDefinition} holding additional (property)path information used for creating the + * index. The path itself is the properties {@literal "dot"} path representation from its root document. + * + * @author Christoph Strobl + * @since 1.5 + */ + public static class IndexDefinitionHolder implements IndexDefinition { + + private String path; + private IndexDefinition indexDefinition; + + /** + * Create + * + * @param path + */ + public IndexDefinitionHolder(String path) { + this.path = path; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.index.IndexDefinition#getCollection() + */ + @Override + public String getCollection() { + return indexDefinition != null ? indexDefinition.getCollection() : null; + } + + /** + * Get the {@liteal "dot"} path used to create the index. + * + * @return + */ + public String getPath() { + return path; + } + + /** + * Get the {@literal raw} {@link IndexDefinition}. + * + * @return + */ + public IndexDefinition getIndexDefinition() { + return indexDefinition; + } + + /** + * Set the {@literal raw} {@link IndexDefinition}. + * + * @param indexDefinition + */ + public void setIndexDefinition(IndexDefinition indexDefinition) { + this.indexDefinition = indexDefinition; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.index.IndexDefinition#getIndexKeys() + */ + @Override + public DBObject getIndexKeys() { + return indexDefinition.getIndexKeys(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.index.IndexDefinition#getIndexOptions() + */ + @Override + public DBObject getIndexOptions() { + return indexDefinition.getIndexOptions(); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java index e013ca833..c84ce35b2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java @@ -31,6 +31,7 @@ import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.WriteResultChecking; import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; +import org.springframework.data.mongodb.core.mapping.Document; import com.mongodb.DBCollection; import com.mongodb.DBObject; @@ -43,6 +44,7 @@ import com.mongodb.WriteConcern; * @author Laurent Canet * @author Oliver Gierke * @author Thomas Darimont + * @author Christoph Strobl */ public class GeoSpatialIndexTests extends AbstractIntegrationTests { @@ -129,6 +131,7 @@ public class GeoSpatialIndexTests extends AbstractIntegrationTests { }); } + @Document static class GeoSpatialEntity2D { public String id; @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2D) public org.springframework.data.geo.Point location; @@ -138,6 +141,7 @@ public class GeoSpatialIndexTests extends AbstractIntegrationTests { } } + @Document static class GeoSpatialEntityHaystack { public String id; public String name; @@ -154,6 +158,7 @@ public class GeoSpatialIndexTests extends AbstractIntegrationTests { double coordinates[]; } + @Document static class GeoSpatialEntity2DSphere { public String id; @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) public GeoJsonPoint location; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexingIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexingIntegrationTests.java index 5555ffd3d..5ac8c4c57 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexingIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 by the original author(s). + * Copyright (c) 2011-2014 by the original author(s). * * 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,8 @@ */ package org.springframework.data.mongodb.core.index; -import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; import org.junit.After; import org.junit.Test; @@ -25,7 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.mongodb.core.CollectionCallback; import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -38,13 +38,13 @@ import com.mongodb.MongoException; * Integration tests for index handling. * * @author Oliver Gierke + * @author Christoph Strobl */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:infrastructure.xml") public class IndexingIntegrationTests { - @Autowired - MongoOperations operations; + @Autowired MongoOperations operations; @After public void tearDown() { @@ -61,11 +61,10 @@ public class IndexingIntegrationTests { assertThat(hasIndex("_firstname", IndexedPerson.class), is(true)); } + @Document class IndexedPerson { - @Field("_firstname") - @Indexed - String firstname; + @Field("_firstname") @Indexed String firstname; } /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java index 885f77d57..a5591f73d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2014 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. @@ -21,18 +21,27 @@ import static org.junit.Assert.*; import java.util.Collections; import java.util.Date; +import org.hamcrest.core.IsEqual; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.context.ApplicationContext; +import org.springframework.data.geo.Point; import org.springframework.data.mapping.context.MappingContextEvent; import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.DB; +import com.mongodb.DBCollection; import com.mongodb.DBObject; /** @@ -41,24 +50,45 @@ import com.mongodb.DBObject; * @author Oliver Gierke * @author Philipp Schneider * @author Johno Crawford + * @author Christoph Strobl */ @RunWith(MockitoJUnitRunner.class) public class MongoPersistentEntityIndexCreatorUnitTests { - @Mock MongoDbFactory factory; - @Mock ApplicationContext context; + private @Mock MongoDbFactory factory; + private @Mock ApplicationContext context; + private @Mock DB db; + private @Mock DBCollection collection; + + ArgumentCaptor keysCaptor; + ArgumentCaptor optionsCaptor; + ArgumentCaptor collectionCaptor; + + @Before + public void setUp() { + + keysCaptor = ArgumentCaptor.forClass(DBObject.class); + optionsCaptor = ArgumentCaptor.forClass(DBObject.class); + collectionCaptor = ArgumentCaptor.forClass(String.class); + + Mockito.when(factory.getDb()).thenReturn(db); + Mockito.when(db.getCollection(collectionCaptor.capture())).thenReturn(collection); + + Mockito.doNothing().when(collection).ensureIndex(keysCaptor.capture(), optionsCaptor.capture()); + } @Test public void buildsIndexDefinitionUsingFieldName() { MongoMappingContext mappingContext = prepareMappingContext(Person.class); - DummyMongoPersistentEntityIndexCreator creator = new DummyMongoPersistentEntityIndexCreator(mappingContext, factory); - assertThat(creator.indexDefinition, is(notNullValue())); - assertThat(creator.indexDefinition.keySet(), hasItem("fieldname")); - assertThat(creator.name, is("indexName")); - assertThat(creator.background, is(false)); - assertThat(creator.expireAfterSeconds, is(-1)); + new MongoPersistentEntityIndexCreator(mappingContext, factory); + + assertThat(keysCaptor.getValue(), is(notNullValue())); + assertThat(keysCaptor.getValue().keySet(), hasItem("fieldname")); + assertThat(optionsCaptor.getValue().get("name").toString(), is("indexName")); + assertThat(optionsCaptor.getValue().get("background"), nullValue()); + assertThat(optionsCaptor.getValue().get("expireAfterSeconds"), nullValue()); } @Test @@ -67,7 +97,7 @@ public class MongoPersistentEntityIndexCreatorUnitTests { MongoMappingContext mappingContext = new MongoMappingContext(); MongoMappingContext personMappingContext = prepareMappingContext(Person.class); - DummyMongoPersistentEntityIndexCreator creator = new DummyMongoPersistentEntityIndexCreator(mappingContext, factory); + MongoPersistentEntityIndexCreator creator = new MongoPersistentEntityIndexCreator(mappingContext, factory); MongoPersistentEntity entity = personMappingContext.getPersistentEntity(Person.class); MappingContextEvent, MongoPersistentProperty> event = new MappingContextEvent, MongoPersistentProperty>( @@ -75,7 +105,7 @@ public class MongoPersistentEntityIndexCreatorUnitTests { creator.onApplicationEvent(event); - assertThat(creator.indexDefinition, is(nullValue())); + Mockito.verifyZeroInteractions(collection); } /** @@ -87,7 +117,7 @@ public class MongoPersistentEntityIndexCreatorUnitTests { MongoMappingContext mappingContext = new MongoMappingContext(); mappingContext.initialize(); - MongoPersistentEntityIndexCreator creator = new DummyMongoPersistentEntityIndexCreator(mappingContext, factory); + MongoPersistentEntityIndexCreator creator = new MongoPersistentEntityIndexCreator(mappingContext, factory); assertThat(creator.isIndexCreatorFor(mappingContext), is(true)); assertThat(creator.isIndexCreatorFor(new MongoMappingContext()), is(false)); } @@ -99,12 +129,13 @@ public class MongoPersistentEntityIndexCreatorUnitTests { public void triggersBackgroundIndexingIfConfigured() { MongoMappingContext mappingContext = prepareMappingContext(AnotherPerson.class); - DummyMongoPersistentEntityIndexCreator creator = new DummyMongoPersistentEntityIndexCreator(mappingContext, factory); + new MongoPersistentEntityIndexCreator(mappingContext, factory); - assertThat(creator.indexDefinition, is(notNullValue())); - assertThat(creator.indexDefinition.keySet(), hasItem("lastname")); - assertThat(creator.name, is("lastname")); - assertThat(creator.background, is(true)); + assertThat(keysCaptor.getValue(), is(notNullValue())); + assertThat(keysCaptor.getValue().keySet(), hasItem("lastname")); + assertThat(optionsCaptor.getValue().get("name").toString(), is("lastname")); + assertThat(optionsCaptor.getValue().get("background"), IsEqual. equalTo(true)); + assertThat(optionsCaptor.getValue().get("expireAfterSeconds"), nullValue()); } /** @@ -114,11 +145,25 @@ public class MongoPersistentEntityIndexCreatorUnitTests { public void expireAfterSecondsIfConfigured() { MongoMappingContext mappingContext = prepareMappingContext(Milk.class); - DummyMongoPersistentEntityIndexCreator creator = new DummyMongoPersistentEntityIndexCreator(mappingContext, factory); + new MongoPersistentEntityIndexCreator(mappingContext, factory); - assertThat(creator.indexDefinition, is(notNullValue())); - assertThat(creator.indexDefinition.keySet(), hasItem("expiry")); - assertThat(creator.expireAfterSeconds, is(60)); + assertThat(keysCaptor.getValue(), is(notNullValue())); + assertThat(keysCaptor.getValue().keySet(), hasItem("expiry")); + assertThat(optionsCaptor.getValue().get("expireAfterSeconds"), IsEqual. equalTo(60L)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void createsNotNestedGeoSpatialIndexCorrectly() { + + MongoMappingContext mappingContext = prepareMappingContext(Wrapper.class); + new MongoPersistentEntityIndexCreator(mappingContext, factory); + + assertThat(keysCaptor.getValue(), equalTo(new BasicDBObjectBuilder().add("company.address.location", "2d").get())); + assertThat(optionsCaptor.getValue(), equalTo(new BasicDBObjectBuilder().add("name", "location").add("min", -180) + .add("max", 180).add("bits", 26).get())); } private static MongoMappingContext prepareMappingContext(Class type) { @@ -130,41 +175,46 @@ public class MongoPersistentEntityIndexCreatorUnitTests { return mappingContext; } + @Document static class Person { - @Indexed(name = "indexName") @Field("fieldname") String field; + @Indexed(name = "indexName")// + @Field("fieldname")// + String field; } + @Document static class AnotherPerson { @Indexed(background = true) String lastname; } + @Document static class Milk { @Indexed(expireAfterSeconds = 60) Date expiry; } - static class DummyMongoPersistentEntityIndexCreator extends MongoPersistentEntityIndexCreator { + @Document + static class Wrapper { + + String id; + Company company; + + } + + static class Company { - DBObject indexDefinition; String name; - boolean background; - int expireAfterSeconds; + Address address; + } - public DummyMongoPersistentEntityIndexCreator(MongoMappingContext mappingContext, MongoDbFactory mongoDbFactory) { - super(mappingContext, mongoDbFactory); - } + static class Address { - @Override - protected void ensureIndex(String collection, String name, DBObject indexDefinition, boolean unique, - boolean dropDups, boolean sparse, boolean background, int expireAfterSeconds) { + String street; + String city; - this.name = name; - this.indexDefinition = indexDefinition; - this.background = background; - this.expireAfterSeconds = expireAfterSeconds; - } + @GeoSpatialIndexed Point location; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java new file mode 100644 index 000000000..162beadac --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -0,0 +1,390 @@ +/* + * Copyright 2014 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 static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.hamcrest.collection.IsEmptyCollection.*; +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsInstanceOf.*; +import static org.junit.Assert.*; + +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.CompoundIndexResolutionTests; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.GeoSpatialIndexResolutionTests; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.IndexResolutionTests; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.MixedIndexResolutionTests; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +import com.mongodb.BasicDBObjectBuilder; + +/** + * @author Christoph Strobl + */ +@RunWith(Suite.class) +@SuiteClasses({ IndexResolutionTests.class, GeoSpatialIndexResolutionTests.class, CompoundIndexResolutionTests.class, + MixedIndexResolutionTests.class }) +public class MongoPersistentEntityIndexResolverUnitTests { + + /** + * Test resolution of {@link Indexed}. + * + * @author Christoph Strobl + */ + public static class IndexResolutionTests { + + /** + * @see DATAMONGO-899 + */ + @Test + public void indexPathOnLevelZeroIsResolvedCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexOnLevelZero.class); + + assertThat(indexDefinitions, hasSize(1)); + assertIndexPathAndCollection("indexedProperty", "Zero", indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void indexPathOnLevelOneIsResolvedCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexOnLevelOne.class); + + assertThat(indexDefinitions, hasSize(1)); + assertIndexPathAndCollection("zero.indexedProperty", "One", indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void depplyNestedIndexPathIsResolvedCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexOnLevelTwo.class); + + assertThat(indexDefinitions, hasSize(1)); + assertIndexPathAndCollection("one.zero.indexedProperty", "Two", indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void resolvesIndexPathNameForNamedPropertiesCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexOnLevelOneWithExplicitlyNamedField.class); + + assertThat(indexDefinitions, hasSize(1)); + assertIndexPathAndCollection("customZero.customFieldName", "indexOnLevelOneWithExplicitlyNamedField", + indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void resolvesIndexDefinitionCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexOnLevelZero.class); + + IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); + assertThat(indexDefinition.getIndexOptions(), equalTo(new BasicDBObjectBuilder().add("name", "indexedProperty") + .get())); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void resolvesIndexDefinitionOptionsCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(WithOptionsOnIndexedProperty.class); + + IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); + assertThat(indexDefinition.getIndexOptions(), + equalTo(new BasicDBObjectBuilder().add("name", "indexedProperty").add("unique", true).add("dropDups", true) + .add("sparse", true).add("background", true).add("expireAfterSeconds", 10L).get())); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void resolvesIndexCollectionNameCorrectlyWhenDefinedInAnnotation() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(WithOptionsOnIndexedProperty.class); + assertThat(indexDefinitions.get(0).getCollection(), equalTo("CollectionOverride")); + } + + @Document(collection = "Zero") + static class IndexOnLevelZero { + @Indexed String indexedProperty; + } + + @Document(collection = "One") + static class IndexOnLevelOne { + IndexOnLevelZero zero; + } + + @Document(collection = "Two") + static class IndexOnLevelTwo { + IndexOnLevelOne one; + } + + @Document(collection = "WithOptionsOnIndexedProperty") + static class WithOptionsOnIndexedProperty { + + @Indexed(background = true, collection = "CollectionOverride", direction = IndexDirection.DESCENDING, + dropDups = true, expireAfterSeconds = 10, sparse = true, unique = true)// + String indexedProperty; + } + + @Document + static class IndexOnLevelOneWithExplicitlyNamedField { + + @Field("customZero") IndexOnLevelZeroWithExplicityNamedField zero; + } + + static class IndexOnLevelZeroWithExplicityNamedField { + + @Indexed @Field("customFieldName") String namedProperty; + } + + } + + /** + * Test resolution of {@link GeoSpatialIndexed}. + * + * @author Christoph Strobl + */ + public static class GeoSpatialIndexResolutionTests { + + /** + * @see DATAMONGO-899 + */ + @Test + public void geoSpatialIndexPathOnLevelZeroIsResolvedCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(GeoSpatialIndexOnLevelZero.class); + + assertThat(indexDefinitions, hasSize(1)); + assertIndexPathAndCollection("geoIndexedProperty", "Zero", indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void geoSpatialIndexPathOnLevelOneIsResolvedCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(GeoSpatialIndexOnLevelOne.class); + + assertThat(indexDefinitions, hasSize(1)); + assertIndexPathAndCollection("zero.geoIndexedProperty", "One", indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void depplyNestedGeoSpatialIndexPathIsResolvedCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(GeoSpatialIndexOnLevelTwo.class); + + assertThat(indexDefinitions, hasSize(1)); + assertIndexPathAndCollection("one.zero.geoIndexedProperty", "Two", indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void resolvesIndexDefinitionOptionsCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(WithOptionsOnGeoSpatialIndexProperty.class); + + IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); + + assertThat( + indexDefinition.getIndexOptions(), + equalTo(new BasicDBObjectBuilder().add("name", "location").add("min", 1).add("max", 100).add("bits", 2).get())); + } + + @Document(collection = "Zero") + static class GeoSpatialIndexOnLevelZero { + @GeoSpatialIndexed Point geoIndexedProperty; + } + + @Document(collection = "One") + static class GeoSpatialIndexOnLevelOne { + GeoSpatialIndexOnLevelZero zero; + } + + @Document(collection = "Two") + static class GeoSpatialIndexOnLevelTwo { + GeoSpatialIndexOnLevelOne one; + } + + @Document(collection = "WithOptionsOnGeoSpatialIndexProperty") + static class WithOptionsOnGeoSpatialIndexProperty { + + @GeoSpatialIndexed(collection = "CollectionOverride", bits = 2, max = 100, min = 1, + type = GeoSpatialIndexType.GEO_2D)// + Point location; + } + + } + + /** + * Test resolution of {@link CompoundIndexes}. + * + * @author Christoph Strobl + */ + public static class CompoundIndexResolutionTests { + + /** + * @see DATAMONGO-899 + */ + @Test + public void compoundIndexPathOnLevelZeroIsResolvedCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(CompoundIndexOnLevelZero.class); + + assertThat(indexDefinitions, hasSize(1)); + assertIndexPathAndCollection("compound_index", "CompoundIndexOnLevelZero", indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void compoundIndexOptionsResolvedCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(CompoundIndexOnLevelZero.class); + + IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); + assertThat(indexDefinition.getIndexOptions(), + equalTo(new BasicDBObjectBuilder().add("name", "compound_index").add("unique", true).add("dropDups", true) + .add("sparse", true).add("background", true).add("expireAfterSeconds", 10L).get())); + assertThat(indexDefinition.getIndexKeys(), equalTo(new BasicDBObjectBuilder().add("foo", 1).add("bar", -1).get())); + } + + @Document(collection = "CompoundIndexOnLevelZero") + @CompoundIndexes({ @CompoundIndex(name = "compound_index", def = "{'foo': 1, 'bar': -1}", background = true, + dropDups = true, expireAfterSeconds = 10, sparse = true, unique = true) }) + static class CompoundIndexOnLevelZero {} + } + + public static class MixedIndexResolutionTests { + + /** + * @see DATAMONGO-899 + */ + @Test + public void multipleIndexesResolvedCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(MixedIndexRoot.class); + + assertThat(indexDefinitions, hasSize(2)); + assertThat(indexDefinitions.get(0).getIndexDefinition(), instanceOf(Index.class)); + assertThat(indexDefinitions.get(1).getIndexDefinition(), instanceOf(GeospatialIndex.class)); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void cyclicPropertyReferenceOverDBRefShouldNotBeTraversed() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(Inner.class); + assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions.get(0).getIndexDefinition().getCollection(), equalTo("inner")); + assertThat(indexDefinitions.get(0).getIndexDefinition().getIndexKeys(), + equalTo(new BasicDBObjectBuilder().add("outer", 1).get())); + } + + /** + * @see DATAMONGO-899 + */ + @Test + public void associationsShouldNotBeTraversed() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(Outer.class); + assertThat(indexDefinitions, empty()); + } + + @Document + static class MixedIndexRoot { + + @Indexed String first; + NestedGeoIndex nestedGeo; + } + + static class NestedGeoIndex { + + @GeoSpatialIndexed Point location; + } + + @Document + static class Outer { + + @DBRef Inner inner; + } + + @Document + static class Inner { + + @Indexed Outer outer; + } + + } + + private static List prepareMappingContextAndResolveIndexForType(Class type) { + + MongoMappingContext mappingContext = prepareMappingContext(type); + MongoPersistentEntityIndexResolver resolver = new MongoPersistentEntityIndexResolver(mappingContext); + return resolver.resolveIndexForEntity(mappingContext.getPersistentEntity(type)); + } + + private static MongoMappingContext prepareMappingContext(Class type) { + + MongoMappingContext mappingContext = new MongoMappingContext(); + mappingContext.setInitialEntitySet(Collections.singleton(type)); + mappingContext.initialize(); + + return mappingContext; + } + + private static void assertIndexPathAndCollection(String expectedPath, String expectedCollection, + IndexDefinitionHolder holder) { + + assertThat(holder.getPath(), equalTo(expectedPath)); + assertThat(holder.getCollection(), equalTo(expectedCollection)); + } +} diff --git a/src/docbkx/reference/mapping.xml b/src/docbkx/reference/mapping.xml index 775df2286..91675e4d6 100644 --- a/src/docbkx/reference/mapping.xml +++ b/src/docbkx/reference/mapping.xml @@ -312,6 +312,11 @@ public class Person { document, making searches faster. + + Automatic index creation is only done for types annotated with + @Document. + +
Mapping annotation overview