diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/HashIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/HashIndexed.java
new file mode 100644
index 000000000..ea848858c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/HashIndexed.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2019 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
+ *
+ * https://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.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker interface for a property that should be used as key for a
+ * Hashed Index. If used on a simple property the index
+ * uses a hashing function to compute the hash of the value of the index field. Added to a property of complex type the
+ * embedded document is collapsed and the hash computed for the entire object.
+ *
+ *
+ *
+ *
+ *
+ * @Document
+ * public class DomainType {
+ *
+ * @HashIndexed @Id String id;
+ * }
+ *
+ *
+ *
+ * {@link HashIndexed} can also be used as meta {@link java.lang.annotation.Annotation} to create composed annotations.
+ *
+ *
+ *
+ *
+ * @Indexed
+ * @HashIndexed
+ * @Retention(RetentionPolicy.RUNTIME)
+ * public @interface IndexAndHash {
+ *
+ * @AliasFor(annotation = Indexed.class, attribute = "name")
+ * String name() default "";
+ * }
+ *
+ * @Document
+ * public class DomainType {
+ *
+ * @ComposedHashIndexed(name = "idx-name") String value;
+ * }
+ *
+ *
+ *
+ * @author Christoph Strobl
+ * @since 2.2
+ * @see HashedIndex
+ */
+@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface HashIndexed {
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/HashedIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/HashedIndex.java
new file mode 100644
index 000000000..1a28bc1d6
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/HashedIndex.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2019 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
+ *
+ * https://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.util.Assert;
+
+/**
+ * {@link IndexDefinition} implementation for MongoDB
+ * Hashed Indexes maintaining entries with hashes of
+ * the values of the indexed field.
+ *
+ * @author Christoph Strobl
+ * @since 2.2
+ */
+public class HashedIndex implements IndexDefinition {
+
+ private final String field;
+
+ private HashedIndex(String field) {
+
+ Assert.hasText(field, "Field must not be null nor empty!");
+ this.field = field;
+ }
+
+ /**
+ * Creates a new {@link HashedIndex} for the given field.
+ *
+ * @param field must not be {@literal null} nor empty.
+ * @return
+ */
+ public static HashedIndex hashed(String field) {
+ return new HashedIndex(field);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.index.IndexDefinition#getIndexKeys()
+ */
+ @Override
+ public Document getIndexKeys() {
+ return new Document(field, "hashed");
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.index.IndexDefinition#getIndexOptions()
+ */
+ @Override
+ public Document getIndexOptions() {
+ return new Document();
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java
index bf6593c16..3dd55684c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java
@@ -30,7 +30,7 @@ import org.springframework.util.ObjectUtils;
public final class IndexField {
enum Type {
- GEO, TEXT, DEFAULT;
+ GEO, TEXT, DEFAULT, HASH;
}
private final String key;
@@ -49,7 +49,9 @@ public final class IndexField {
if (Type.GEO.equals(type) || Type.TEXT.equals(type)) {
Assert.isNull(direction, "Geo/Text indexes must not have a direction!");
} else {
- Assert.notNull(direction, "Default indexes require a direction");
+ if (!Type.HASH.equals(type)) {
+ Assert.notNull(direction, "Default indexes require a direction");
+ }
}
this.key = key;
@@ -65,6 +67,17 @@ public final class IndexField {
return new IndexField(key, order, Type.DEFAULT);
}
+ /**
+ * Creates a {@literal hashed} {@link IndexField} for the given key.
+ *
+ * @param key must not be {@literal null} or empty.
+ * @return new instance of {@link IndexField}.
+ * @since 2.2
+ */
+ static IndexField hashed(String key) {
+ return new IndexField(key, null, Type.HASH);
+ }
+
/**
* Creates a geo {@link IndexField} for the given key.
*
@@ -120,6 +133,16 @@ public final class IndexField {
return Type.TEXT.equals(type);
}
+ /**
+ * Returns whether the {@link IndexField} is a {@literal hashed}.
+ *
+ * @return {@literal true} if {@link IndexField} is hashed.
+ * @since 2.2
+ */
+ public boolean isHashed() {
+ return Type.HASH.equals(type);
+ }
+
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
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 001605d8e..7554a546e 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
@@ -96,12 +96,17 @@ public class IndexInfo {
} else {
- Double keyValue = new Double(value.toString());
+ if (ObjectUtils.nullSafeEquals("hashed", value)) {
+ indexFields.add(IndexField.hashed(key));
+ } else {
- if (ONE.equals(keyValue)) {
- indexFields.add(IndexField.create(key, ASC));
- } else if (MINUS_ONE.equals(keyValue)) {
- indexFields.add(IndexField.create(key, DESC));
+ 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));
+ }
}
}
}
@@ -206,6 +211,14 @@ public class IndexInfo {
return Optional.ofNullable(expireAfter);
}
+ /**
+ * @return {@literal true} if a hashed index field is present.
+ * @since 2.2
+ */
+ public boolean isHashed() {
+ return getIndexFields().stream().anyMatch(IndexField::isHashed);
+ }
+
@Override
public String toString() {
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
index 73edce0f4..0d5fd5e5d 100644
--- 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
@@ -33,7 +33,6 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.Association;
@@ -140,10 +139,10 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
persistentProperty.getFieldName(), Path.of(persistentProperty), root.getCollection(), guard));
}
- IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty(
+ List indexDefinitions = createIndexDefinitionHolderForProperty(
persistentProperty.getFieldName(), root.getCollection(), persistentProperty);
- if (indexDefinitionHolder != null) {
- indexes.add(indexDefinitionHolder);
+ if (!indexDefinitions.isEmpty()) {
+ indexes.addAll(indexDefinitions);
}
} catch (CyclicPropertyReferenceException e) {
LOGGER.info(e.getMessage());
@@ -170,14 +169,14 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(dotPath, collection, entity));
entity.doWithProperties((PropertyHandler) property -> this
- .guradAndPotentiallyAddIndexForProperty(property, dotPath, path, collection, indexInformation, guard));
+ .guardAndPotentiallyAddIndexForProperty(property, dotPath, path, collection, indexInformation, guard));
indexInformation.addAll(resolveIndexesForDbrefs(dotPath, collection, entity));
return indexInformation;
}
- private void guradAndPotentiallyAddIndexForProperty(MongoPersistentProperty persistentProperty, String dotPath,
+ private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty persistentProperty, String dotPath,
Path path, String collection, List indexes, CycleGuard guard) {
String propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") + persistentProperty.getFieldName();
@@ -194,25 +193,31 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
}
- IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty(propertyDotPath, collection,
+ List indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath, collection,
persistentProperty);
- if (indexDefinitionHolder != null) {
- indexes.add(indexDefinitionHolder);
+ if (!indexDefinitions.isEmpty()) {
+ indexes.addAll(indexDefinitions);
}
}
@Nullable
- private IndexDefinitionHolder createIndexDefinitionHolderForProperty(String dotPath, String collection,
+ private List createIndexDefinitionHolderForProperty(String dotPath, String collection,
MongoPersistentProperty persistentProperty) {
+ List indices = new ArrayList<>(2);
+
if (persistentProperty.isAnnotationPresent(Indexed.class)) {
- return createIndexDefinition(dotPath, collection, persistentProperty);
+ indices.add(createIndexDefinition(dotPath, collection, persistentProperty));
} else if (persistentProperty.isAnnotationPresent(GeoSpatialIndexed.class)) {
- return createGeoSpatialIndexDefinition(dotPath, collection, persistentProperty);
+ indices.add(createGeoSpatialIndexDefinition(dotPath, collection, persistentProperty));
}
- return null;
+ if (persistentProperty.isAnnotationPresent(HashIndexed.class)) {
+ indices.add(createHashedIndexDefinition(dotPath, collection, persistentProperty));
+ }
+
+ return indices;
}
private List potentiallyCreateCompoundIndexDefinitions(String dotPath, String collection,
@@ -327,7 +332,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
*
* @param dotPath The properties {@literal "dot"} path representation from its document root.
* @param fallbackCollection
- * @param type
+ * @param entity
* @return
*/
protected List createCompoundIndexDefinitions(String dotPath, String fallbackCollection,
@@ -410,14 +415,14 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
*
* @param dotPath The properties {@literal "dot"} path representation from its document root.
* @param collection
- * @param persitentProperty
+ * @param persistentProperty
* @return
*/
@Nullable
protected IndexDefinitionHolder createIndexDefinition(String dotPath, String collection,
- MongoPersistentProperty persitentProperty) {
+ MongoPersistentProperty persistentProperty) {
- Indexed index = persitentProperty.findAnnotation(Indexed.class);
+ Indexed index = persistentProperty.findAnnotation(Indexed.class);
if (index == null) {
return null;
@@ -427,7 +432,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
IndexDirection.ASCENDING.equals(index.direction()) ? Sort.Direction.ASC : Sort.Direction.DESC);
if (!index.useGeneratedName()) {
- indexDefinition.named(pathAwareIndexName(index.name(), dotPath, persitentProperty.getOwner(), persitentProperty));
+ indexDefinition
+ .named(pathAwareIndexName(index.name(), dotPath, persistentProperty.getOwner(), persistentProperty));
}
if (index.unique()) {
@@ -455,7 +461,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
Duration timeout = computeIndexTimeout(index.expireAfter(),
- getEvaluationContextForProperty(persitentProperty.getOwner()));
+ getEvaluationContextForProperty(persistentProperty.getOwner()));
if (!timeout.isZero() && !timeout.isNegative()) {
indexDefinition.expire(timeout);
}
@@ -464,6 +470,29 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
}
+ /**
+ * Creates {@link HashedIndex} wrapped in {@link IndexDefinitionHolder} out of {@link HashIndexed} for given
+ * {@link MongoPersistentProperty}.
+ *
+ * @param dotPath The properties {@literal "dot"} path representation from its document root.
+ * @param collection
+ * @param persistentProperty
+ * @return
+ * @since 2.2
+ */
+ @Nullable
+ protected IndexDefinitionHolder createHashedIndexDefinition(String dotPath, String collection,
+ MongoPersistentProperty persistentProperty) {
+
+ HashIndexed index = persistentProperty.findAnnotation(HashIndexed.class);
+
+ if (index == null) {
+ return null;
+ }
+
+ return new IndexDefinitionHolder(dotPath, HashedIndex.hashed(dotPath), collection);
+ }
+
/**
* Get the default {@link EvaluationContext}.
*
@@ -588,11 +617,11 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
propertyDotPath));
}
- IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty(propertyDotPath, collection,
+ List indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath, collection,
property);
- if (indexDefinitionHolder != null) {
- indexes.add(indexDefinitionHolder);
+ if (!indexDefinitions.isEmpty()) {
+ indexes.addAll(indexDefinitions);
}
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsUnitTests.java
index fe5527293..aac1b67ed 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsUnitTests.java
@@ -31,6 +31,7 @@ import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.index.HashedIndex;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -112,6 +113,14 @@ public class DefaultIndexOperationsUnitTests {
.isEqualTo(com.mongodb.client.model.Collation.builder().locale("en_US").build());
}
+ @Test // DATAMONGO-1183
+ public void shouldCreateHashedIndexCorrectly() {
+
+ indexOpsFor(Jedi.class).ensureIndex(HashedIndex.hashed("name"));
+
+ verify(collection).createIndex(eq(new Document("firstname", "hashed")), any());
+ }
+
private DefaultIndexOperations indexOpsFor(Class> type) {
return new DefaultIndexOperations(template, template.getCollectionName(type), type);
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexFieldUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexFieldUnitTests.java
index 8043012ab..af2732f8d 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexFieldUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexFieldUnitTests.java
@@ -25,6 +25,7 @@ import org.springframework.data.domain.Sort.Direction;
* Unit tests for {@link IndexField}.
*
* @author Oliver Gierke
+ * @author Christoph Strobl
*/
@SuppressWarnings("deprecation")
public class IndexFieldUnitTests {
@@ -68,4 +69,19 @@ public class IndexFieldUnitTests {
assertThat(first, is(second));
assertThat(second, is(first));
}
+
+ @Test // DATAMONGO-1183
+ public void correctTypeForHashedFields() {
+ assertThat(IndexField.hashed("key").isHashed(), is(true));
+ }
+
+ @Test // DATAMONGO-1183
+ public void correctEqualsForHashedFields() {
+
+ IndexField first = IndexField.hashed("bar");
+ IndexField second = IndexField.hashed("bar");
+
+ assertThat(first, is(second));
+ assertThat(second, is(first));
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java
index ef0b48030..c1051d63f 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java
@@ -35,6 +35,7 @@ public class IndexInfoUnitTests {
static final String ID_INDEX = "{ \"v\" : 2, \"key\" : { \"_id\" : 1 }, \"name\" : \"_id_\", \"ns\" : \"db.collection\" }";
static final String INDEX_WITH_PARTIAL_FILTER = "{ \"v\" : 2, \"key\" : { \"k3y\" : 1 }, \"name\" : \"partial-filter-index\", \"ns\" : \"db.collection\", \"partialFilterExpression\" : { \"quantity\" : { \"$gte\" : 10 } } }";
static final String INDEX_WITH_EXPIRATION_TIME = "{ \"v\" : 2, \"key\" : { \"lastModifiedDate\" : 1 },\"name\" : \"expire-after-last-modified\", \"ns\" : \"db.collectio\", \"expireAfterSeconds\" : 3600 }";
+ static final String HASHED_INDEX = "{ \"v\" : 2, \"key\" : { \"score\" : \"hashed\" }, \"name\" : \"score_hashed\", \"ns\" : \"db.collection\" }";
@Test
public void isIndexForFieldsCorrectly() {
@@ -68,6 +69,16 @@ public class IndexInfoUnitTests {
assertThat(getIndexInfo(ID_INDEX).getExpireAfter()).isEmpty();
}
+ @Test // DATAMONGO-1183
+ public void readsHashedIndexCorrectly() {
+ assertThat(getIndexInfo(HASHED_INDEX).getIndexFields()).containsExactly(IndexField.hashed("score"));
+ }
+
+ @Test // DATAMONGO-1183
+ public void hashedIndexIsMarkedAsSuch() {
+ assertThat(getIndexInfo(HASHED_INDEX).isHashed()).isTrue();
+ }
+
private static IndexInfo getIndexInfo(String documentJson) {
return IndexInfo.indexInfoOf(Document.parse(documentJson));
}
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
index 0ed371fc7..5e97728ac 100644
--- 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
@@ -1174,6 +1174,60 @@ public class MongoPersistentEntityIndexResolverUnitTests {
"listWithGeneircTypeElement.entity.property_index");
}
+ @Test // DATAMONGO-1183
+ public void hashedIndexOnId() {
+
+ List indexDefinitions = prepareMappingContextAndResolveIndexForType(
+ WithHashedIndexOnId.class);
+
+ assertThat(indexDefinitions).hasSize(1);
+ assertThat(indexDefinitions.get(0)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document("_id", "hashed"));
+ });
+ }
+
+ @Test // DATAMONGO-1183
+ public void hashedIndex() {
+
+ List indexDefinitions = prepareMappingContextAndResolveIndexForType(WithHashedIndex.class);
+
+ assertThat(indexDefinitions).hasSize(1);
+ assertThat(indexDefinitions.get(0)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document("value", "hashed"));
+ });
+ }
+
+ @Test // DATAMONGO-1183
+ public void hashedIndexAndIndex() {
+
+ List indexDefinitions = prepareMappingContextAndResolveIndexForType(
+ WithHashedIndexAndIndex.class);
+
+ assertThat(indexDefinitions).hasSize(2);
+ assertThat(indexDefinitions.get(0)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document("value", 1));
+ });
+ assertThat(indexDefinitions.get(1)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document("value", "hashed"));
+ });
+ }
+
+ @Test // DATAMONGO-1183
+ public void hashedIndexAndIndexViaComposedAnnotation() {
+
+ List indexDefinitions = prepareMappingContextAndResolveIndexForType(
+ WithComposedHashedIndexAndIndex.class);
+
+ assertThat(indexDefinitions).hasSize(2);
+ assertThat(indexDefinitions.get(0)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document("value", 1));
+ assertThat(it.getIndexOptions()).containsEntry("name", "idx-name");
+ });
+ assertThat(indexDefinitions.get(1)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document("value", "hashed"));
+ });
+ }
+
@Document
static class MixedIndexRoot {
@@ -1376,6 +1430,41 @@ public class MongoPersistentEntityIndexResolverUnitTests {
static class EntityWithGenericTypeWrapperAsElement {
List> listWithGeneircTypeElement;
}
+
+ @Document
+ static class WithHashedIndexOnId {
+
+ @HashIndexed @Id String id;
+ }
+
+ @Document
+ static class WithHashedIndex {
+
+ @HashIndexed String value;
+ }
+
+ @Document
+ static class WithHashedIndexAndIndex {
+
+ @Indexed //
+ @HashIndexed //
+ String value;
+ }
+
+ @Document
+ static class WithComposedHashedIndexAndIndex {
+
+ @ComposedHashIndexed(name = "idx-name") String value;
+ }
+
+ @HashIndexed
+ @Indexed
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface ComposedHashIndexed {
+
+ @AliasFor(annotation = Indexed.class, attribute = "name")
+ String name() default "";
+ }
}
private static List prepareMappingContextAndResolveIndexForType(Class> type) {
diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc
index bbb790b50..bb32ec0f3 100644
--- a/src/main/asciidoc/reference/mapping.adoc
+++ b/src/main/asciidoc/reference/mapping.adoc
@@ -414,6 +414,7 @@ The MappingMongoConverter can use metadata to drive the mapping of objects to do
* `@CompoundIndex` (repeatable): Applied at the type level to declare Compound Indexes.
* `@GeoSpatialIndexed`: Applied at the field level to describe how to geoindex the field.
* `@TextIndexed`: Applied at the field level to mark the field to be included in the text index.
+* `@HashIndexed`: Applied at the field level for usage within a hashed index to partition data across a sharded cluster.
* `@Language`: Applied at the field level to set the language override property for text index.
* `@Transient`: By default all private fields are mapped to the document, this annotation excludes the field where it is applied from being stored in the database
* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved Document.
@@ -602,6 +603,84 @@ public class Person {
----
====
+[[mapping-usage-indexes.hashed-index]]
+=== Hashed Indexes
+
+Hashed indexes allow hash based sharding within a sharded cluster. The hashed field value is used to shard collection results
+in a more random distribution. For details refer to the https://docs.mongodb.com/manual/core/index-hashed/[MongoDB Documentation].
+
+Here's an example that creates a hashed index for `_id`:
+
+.Example Hashed Index Usage
+====
+[source,java]
+----
+@Document
+public class DomainType {
+
+ @HashIndexed @Id String id;
+
+ // ...
+}
+----
+====
+
+Hashed indexes can be created next to other index definitions like shown below, in that case both indices will be created.
+
+.Example Hashed Index Usage togehter with simple index
+====
+[source,java]
+----
+@Document
+public class DomainType {
+
+ @Indexed
+ @HashIndexed
+ String value;
+
+ // ...
+}
+----
+====
+
+In case the above example is too verbose, a compound annotation allows to reduce the number of annotations present.
+
+.Example Composed Hashed Index Usage
+====
+[source,java]
+----
+@Document
+public class DomainType {
+
+ @IndexAndHash(name = "idx...") <1>
+ String value;
+
+ // ...
+}
+
+@Indexed
+@HashIndexed
+@Retention(RetentionPolicy.RUNTIME)
+public @interface IndexAndHash {
+
+ @AliasFor(annotation = Indexed.class, attribute = "name") <1>
+ String name() default "";
+}
+----
+<1> Potentially register an alias for certain attributes of the meta annotation.
+====
+
+[NOTE]
+====
+Although index creation via annotations comes in handy for many scenarios please cosider taking over more control by setting up indices manually via `IndexOperations`.
+
+[source,java]
+----
+mongoOperations.indexOpsFor(Jedi.class)
+ .ensureIndex(HashedIndex.hashed("useTheForce"));
+----
+====
+
[[mapping-usage-indexes.text-index]]
=== Text Indexes