getDocumentIndexInfoConverter() {
- return IndexInfo::indexInfoOf;
- }
-
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexOperationsSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexOperationsSupport.java
new file mode 100644
index 0000000000..4d78ef224f
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexOperationsSupport.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2026-present 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;
+
+import java.util.List;
+
+import org.bson.Document;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.mongodb.UncategorizedMongoDbException;
+import org.springframework.data.mongodb.core.convert.QueryMapper;
+import org.springframework.data.mongodb.core.index.IndexDefinition;
+import org.springframework.data.mongodb.core.mapping.CollectionName;
+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.NumberUtils;
+
+import com.mongodb.client.model.CreateIndexOptions;
+import com.mongodb.client.model.IndexModel;
+import com.mongodb.client.model.IndexOptions;
+
+/**
+ * Base class for index operations.
+ *
+ * This class enables the implementations to use consistent commands and mapping to be implemented easily for the target
+ * execution model.
+ *
+ * @author Mark Paluch
+ * @since 5.1
+ */
+public abstract class IndexOperationsSupport {
+
+ private static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression";
+
+ private final CollectionName collectionName;
+ private final QueryMapper queryMapper;
+ private final MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext;
+ private final @Nullable Class> entityType;
+
+ /**
+ * Creates a new {@code IndexOperationsSupport} for the given {@link CollectionName} and {@link QueryMapper}.
+ *
+ * @param collectionName the collection name to use.
+ * @param queryMapper the query mapper to use for mapping index keys.
+ * @param entityType optional entity entityType to use for mapping.
+ */
+ public IndexOperationsSupport(CollectionName collectionName, QueryMapper queryMapper, @Nullable Class> entityType) {
+
+ Assert.notNull(collectionName, "Collection must not be null");
+ Assert.notNull(queryMapper, "QueryMapper must not be null");
+
+ this.collectionName = collectionName;
+ this.queryMapper = queryMapper;
+ this.mappingContext = queryMapper.getMappingContext();
+ this.entityType = entityType;
+ }
+
+ /**
+ * Map {@link IndexDefinition} to {@link CreateIndexCommand}.
+ *
+ * @param indexDefinition the index definition to map.
+ * @return the mapped {@link CreateIndexCommand}.
+ */
+ protected CreateIndexCommand toCreateIndexCommand(IndexDefinition indexDefinition) {
+
+ IndexOptions indexOptions = mapIndexDefinition(indexDefinition);
+ Document keys = mapKeys(indexDefinition);
+
+ return new DefaultCreateIndexCommand(keys, indexOptions, new CreateIndexOptions());
+ }
+
+ /**
+ * Map {@link IndexDefinition} to {@link IndexOptions}.
+ *
+ * @param indexDefinition the index definition to map.
+ * @return the mapped {@link IndexOptions}.
+ */
+ protected IndexOptions mapIndexDefinition(IndexDefinition indexDefinition) {
+
+ MongoPersistentEntity> entity = getConfiguredEntity();
+
+ IndexOptions indexOptions = IndexConverters.toIndexOptions(indexDefinition);
+
+ indexOptions = addPartialFilterIfPresent(indexOptions, indexDefinition.getIndexOptions(), entity);
+ indexOptions = addDefaultCollationIfRequired(indexOptions, entity);
+
+ return indexOptions;
+ }
+
+ /**
+ * Map index keys to {@link Document}.
+ *
+ * @param indexDefinition the source index definition.
+ * @return the mapped {@link Document}.
+ */
+ protected Document mapKeys(IndexDefinition indexDefinition) {
+ return queryMapper.getMappedSort(indexDefinition.getIndexKeys(), getConfiguredEntity());
+ }
+
+ /**
+ * Return the collection name to use.
+ */
+ protected String getCollectionName() {
+ return collectionName.getCollectionName(mappingContext::getRequiredPersistentEntity);
+ }
+
+ /**
+ * Create the command to alter an index.
+ *
+ * @param name index name.
+ * @param options index options to use.
+ * @return
+ */
+ Document alterIndexCommand(String name, org.springframework.data.mongodb.core.index.IndexOptions options) {
+ Document indexOptions = new Document("name", name);
+ indexOptions.putAll(options.toDocument());
+ return new Document("collMod", getCollectionName()).append("index", indexOptions);
+ }
+
+ /**
+ * Validate the alter index response and throw {@link UncategorizedMongoDbException} if the index could not be
+ * altered.
+ *
+ * @param name index name.
+ * @param result result document from an earlier
+ * {@link #alterIndexCommand(String, org.springframework.data.mongodb.core.index.IndexOptions)}.
+ */
+ static void validateAlterIndexResponse(String name, Document result) {
+ Integer ok = NumberUtils.convertNumberToTargetClass(result.get("ok", (Number) 0), Integer.class);
+ if (ok != 1) {
+ throw new UncategorizedMongoDbException(
+ "Index '%s' could not be modified. Response was %s".formatted(name, result.toJson()), null);
+ }
+ }
+
+ private @Nullable MongoPersistentEntity> getConfiguredEntity() {
+ return entityType != null ? mappingContext.getRequiredPersistentEntity(entityType) : null;
+ }
+
+ private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document sourceOptions,
+ @Nullable MongoPersistentEntity> entity) {
+
+ if (!sourceOptions.containsKey(PARTIAL_FILTER_EXPRESSION_KEY)) {
+ return ops;
+ }
+
+ Assert.isInstanceOf(Document.class, sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY));
+ return ops.partialFilterExpression(
+ queryMapper.getMappedObject((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity));
+ }
+
+ @SuppressWarnings("NullAway")
+ private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops,
+ @Nullable MongoPersistentEntity> entity) {
+
+ if (ops.getCollation() != null || entity == null || !entity.hasCollation()) {
+ return ops;
+ }
+
+ return ops.collation(entity.getCollation().toMongoCollation());
+ }
+
+ /**
+ * Interface for a create index command.
+ */
+ protected interface CreateIndexCommand {
+
+ /**
+ * Return the index keys.
+ */
+ Document keys();
+
+ /**
+ * Return the index options.
+ */
+ IndexOptions indexOptions();
+
+ /**
+ * Return the index creation options.
+ */
+ CreateIndexOptions createIndexOptions();
+
+ /**
+ * Create a new {@code CreateIndexCommand} with the given {@link CreateIndexOptions}.
+ *
+ * @param createIndexOptions the new {@link CreateIndexOptions} to use.
+ * @return a new {@code CreateIndexCommand} with the given {@link CreateIndexOptions} applied.
+ */
+ default CreateIndexCommand withCreateIndexOptions(CreateIndexOptions createIndexOptions) {
+ return new DefaultCreateIndexCommand(keys(), indexOptions(), createIndexOptions);
+ }
+
+ default IndexModel toIndexModel() {
+ return new IndexModel(keys(), indexOptions());
+ }
+
+ default List toIndexModels() {
+ return List.of(toIndexModel());
+ }
+ }
+
+ /**
+ * Value object for a create index command.
+ *
+ * @param keys index keys.
+ * @param indexOptions the index options to use.
+ * @param createIndexOptions index creation options.
+ */
+ private record DefaultCreateIndexCommand(Document keys, IndexOptions indexOptions,
+ CreateIndexOptions createIndexOptions) implements CreateIndexCommand {
+
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
index ee4f2e3ee4..8447ec33f1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
@@ -26,6 +26,7 @@
import org.bson.Document;
import org.jspecify.annotations.Nullable;
+
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
@@ -39,8 +40,8 @@
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.bulk.Bulk;
-import org.springframework.data.mongodb.core.bulk.BulkWriteResult;
import org.springframework.data.mongodb.core.bulk.BulkWriteOptions;
+import org.springframework.data.mongodb.core.bulk.BulkWriteResult;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.index.IndexOperations;
@@ -450,16 +451,26 @@ MongoCollection createView(String name, String source, AggregationPipe
void dropCollection(String collectionName);
/**
- * Returns the operations that can be performed on indexes
+ * Returns the operations that can be performed on indexes.
*
- * @return index operations on the named collection
+ * @return index operations on the named collection.
*/
IndexOperations indexOps(String collectionName);
/**
- * Returns the operations that can be performed on indexes
+ * Returns the operations that can be performed on indexes.
+ *
+ * @param collectionName name of the MongoDB collection, must not be {@literal null}.
+ * @param entityClass the entityClass used for field mapping.
+ * @return index operations on the named collection.
+ * @since 5.1
+ */
+ IndexOperations indexOps(String collectionName, Class> entityClass);
+
+ /**
+ * Returns the operations that can be performed on indexes.
*
- * @return index operations on the named collection associated with the given entity class
+ * @return index operations on the named collection associated with the given entity class.
*/
IndexOperations indexOps(Class> entityClass);
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 7b1a5181e4..45c8b0e899 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
@@ -821,8 +821,8 @@ public IndexOperations indexOps(String collectionName) {
}
@Override
- public IndexOperations indexOps(String collectionName, @Nullable Class> type) {
- return new DefaultIndexOperations(this, collectionName, type);
+ public IndexOperations indexOps(String collectionName, @Nullable Class> entityClass) {
+ return createIndexOperations(collectionName, this.queryMapper, entityClass);
}
@Override
@@ -830,6 +830,11 @@ public IndexOperations indexOps(Class> entityClass) {
return indexOps(getCollectionName(entityClass), entityClass);
}
+ protected IndexOperations createIndexOperations(String collectionName, QueryMapper queryMapper,
+ @Nullable Class> entityClass) {
+ return new DefaultIndexOperations(this, collectionName, queryMapper, entityClass);
+ }
+
@Override
public SearchIndexOperations searchIndexOps(String collectionName) {
return searchIndexOps(null, collectionName);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
index 69d4741bf4..808c38f0ed 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
@@ -92,6 +92,16 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*/
ReactiveIndexOperations indexOps(String collectionName);
+ /**
+ * Returns the reactive operations that can be performed on indexes
+ *
+ * @param collectionName name of the MongoDB collection, must not be {@literal null}.
+ * @param entityClass the entityClass used for field mapping.
+ * @return index operations on the named collection.
+ * @since 5.1
+ */
+ ReactiveIndexOperations indexOps(String collectionName, Class> entityClass);
+
/**
* Returns the reactive operations that can be performed on indexes
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
index 8c879a192d..c1376b0cda 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
@@ -510,14 +510,25 @@ QueryOperations getQueryOperations() {
return queryOperations;
}
+
@Override
public ReactiveIndexOperations indexOps(String collectionName) {
- return new DefaultReactiveIndexOperations(this, collectionName, this.queryMapper);
+ return createIndexOperations(collectionName, this.queryMapper, null);
+ }
+
+ @Override
+ public ReactiveIndexOperations indexOps(String collectionName, @Nullable Class> entityClass) {
+ return createIndexOperations(collectionName, this.queryMapper, entityClass);
}
@Override
public ReactiveIndexOperations indexOps(Class> entityClass) {
- return new DefaultReactiveIndexOperations(this, getCollectionName(entityClass), this.queryMapper, entityClass);
+ return indexOps(getCollectionName(entityClass), entityClass);
+ }
+
+ protected ReactiveIndexOperations createIndexOperations(String collectionName, QueryMapper queryMapper,
+ @Nullable Class> entityClass) {
+ return new DefaultReactiveIndexOperations(this, collectionName, queryMapper, entityClass);
}
@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java
index 451d88e44a..2d80ba0050 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java
@@ -31,7 +31,7 @@ public interface IndexOperationsProvider {
* Returns the operations that can be performed on indexes.
*
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
- * @return index operations on the named collection
+ * @return index operations on the named collection.
*/
default IndexOperations indexOps(String collectionName) {
return indexOps(collectionName, null);
@@ -41,10 +41,10 @@ default IndexOperations indexOps(String collectionName) {
* Returns the operations that can be performed on indexes.
*
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
- * @param type the type used for field mapping. Can be {@literal null}.
- * @return index operations on the named collection
+ * @param entityClass the entityClass used for field mapping. Can be {@literal null}.
+ * @return index operations on the named collection.
* @since 3.2
*/
- IndexOperations indexOps(String collectionName, @Nullable Class> type);
+ IndexOperations indexOps(String collectionName, @Nullable Class> entityClass);
}
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 9e66e259b9..298d9bb31f 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
@@ -18,13 +18,18 @@
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
+import java.util.List;
+import java.util.function.BiConsumer;
+
import org.bson.Document;
+import org.bson.conversions.Bson;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
@@ -37,6 +42,8 @@
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.CreateIndexOptions;
+import com.mongodb.client.model.IndexModel;
import com.mongodb.client.model.IndexOptions;
/**
@@ -64,7 +71,7 @@ void setUp() {
when(factory.getMongoDatabase()).thenReturn(db);
when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator);
when(db.getCollection(any(), any(Class.class))).thenReturn(collection);
- when(collection.createIndex(any(), any(IndexOptions.class))).thenReturn("OK");
+ when(collection.createIndexes(anyList(), any(CreateIndexOptions.class))).thenReturn(List.of("OK"));
this.mappingContext = new MongoMappingContext();
this.converter = spy(new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext));
@@ -76,7 +83,7 @@ void indexOperationsMapFieldNameCorrectly() {
indexOpsFor(Jedi.class).ensureIndex(new Index("name", Direction.DESC));
- verify(collection).createIndex(eq(new Document("firstname", -1)), any());
+ verifyCreateIndex((keys, options) -> assertThat(keys).isEqualTo(new Document("firstname", -1)));
}
@Test // DATAMONGO-1854
@@ -84,10 +91,7 @@ void ensureIndexDoesNotSetCollectionIfNoDefaultDefined() {
indexOpsFor(Jedi.class).ensureIndex(new Index("firstname", Direction.DESC));
- ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class);
- verify(collection).createIndex(any(), options.capture());
-
- assertThat(options.getValue().getCollation()).isNull();
+ verifyCreateIndex((keys, options) -> assertThat(options.getCollation()).isNull());
}
@Test // DATAMONGO-1854
@@ -95,11 +99,8 @@ void ensureIndexUsesDefaultCollationIfNoneDefinedInOptions() {
indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC));
- ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class);
- verify(collection).createIndex(any(), options.capture());
-
- assertThat(options.getValue().getCollation())
- .isEqualTo(com.mongodb.client.model.Collation.builder().locale("de_AT").build());
+ verifyCreateIndex((keys, options) -> assertThat(options.getCollation())
+ .isEqualTo(com.mongodb.client.model.Collation.builder().locale("de_AT").build()));
}
@Test // DATAMONGO-1854
@@ -107,11 +108,8 @@ void ensureIndexDoesNotUseDefaultCollationIfExplicitlySpecifiedInTheIndex() {
indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC).collation(Collation.of("en_US")));
- ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class);
- verify(collection).createIndex(any(), options.capture());
-
- assertThat(options.getValue().getCollation())
- .isEqualTo(com.mongodb.client.model.Collation.builder().locale("en_US").build());
+ verifyCreateIndex((keys, options) -> assertThat(options.getCollation())
+ .isEqualTo(com.mongodb.client.model.Collation.builder().locale("en_US").build()));
}
@Test // DATAMONGO-1183
@@ -119,7 +117,7 @@ void shouldCreateHashedIndexCorrectly() {
indexOpsFor(Jedi.class).ensureIndex(HashedIndex.hashed("name"));
- verify(collection).createIndex(eq(new Document("firstname", "hashed")), any());
+ verifyCreateIndex((keys, options) -> assertThat(keys).isEqualTo(new Document("firstname", "hashed")));
}
@Test // GH-4698
@@ -131,6 +129,16 @@ void shouldConsiderGivenCollectionName() {
verify(db).getCollection(eq("foo"), any(Class.class));
}
+ private void verifyCreateIndex(BiConsumer consumer) {
+
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
+
+ verify(collection).createIndexes(captor.capture(), any());
+
+ IndexModel indexModel = captor.getValue().get(0);
+ consumer.accept(indexModel.getKeys(), indexModel.getOptions());
+ }
+
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/DefaultReactiveIndexOperationsTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java
index f1d3bf412e..4710d94239 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java
@@ -42,6 +42,8 @@
import com.mongodb.reactivestreams.client.MongoCollection;
/**
+ * Integration tests for {@link DefaultReactiveIndexOperations}.
+ *
* @author Christoph Strobl
* @author Mark Paluch
* @author Mathieu Ouellet
@@ -49,22 +51,22 @@
public class DefaultReactiveIndexOperationsTests {
@Template(initialEntitySet = DefaultIndexOperationsIntegrationTestsSample.class) //
- static ReactiveMongoTestTemplate template;
+ private static ReactiveMongoTestTemplate template;
- String collectionName = template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class);
+ private String collectionName = template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class);
- DefaultReactiveIndexOperations indexOps = new DefaultReactiveIndexOperations(template, collectionName,
- new QueryMapper(template.getConverter()));
+ private DefaultReactiveIndexOperations indexOps = new DefaultReactiveIndexOperations(template, collectionName,
+ new QueryMapper(template.getConverter()), DefaultIndexOperationsIntegrationTestsSample.class);
@BeforeEach
- public void setUp() {
+ void setUp() {
template.getCollection(collectionName).flatMapMany(MongoCollection::dropIndexes) //
.as(StepVerifier::create) //
.verifyComplete();
}
@Test // DATAMONGO-1518
- public void shouldCreateIndexWithCollationCorrectly() {
+ void shouldCreateIndexWithCollationCorrectly() {
IndexDefinition id = new Index().named("with-collation").on("xyz", Direction.ASC)
.collation(Collation.of("de_AT").caseFirst(CaseFirst.off()));
@@ -96,7 +98,7 @@ public void shouldCreateIndexWithCollationCorrectly() {
}
@Test // DATAMONGO-1682, DATAMONGO-2198
- public void shouldApplyPartialFilterCorrectly() {
+ void shouldApplyPartialFilterCorrectly() {
IndexDefinition id = new Index().named("partial-with-criteria").on("k3y", Direction.ASC)
.partial(of(where("q-t-y").gte(10)));
@@ -112,7 +114,7 @@ public void shouldApplyPartialFilterCorrectly() {
}
@Test // DATAMONGO-1682, DATAMONGO-2198
- public void shouldApplyPartialFilterWithMappedPropertyCorrectly() {
+ void shouldApplyPartialFilterWithMappedPropertyCorrectly() {
IndexDefinition id = new Index().named("partial-with-mapped-criteria").on("k3y", Direction.ASC)
.partial(of(where("quantity").gte(10)));
@@ -127,7 +129,7 @@ public void shouldApplyPartialFilterWithMappedPropertyCorrectly() {
}
@Test // DATAMONGO-1682, DATAMONGO-2198
- public void shouldApplyPartialDBOFilterCorrectly() {
+ void shouldApplyPartialDBOFilterCorrectly() {
IndexDefinition id = new Index().named("partial-with-dbo").on("k3y", Direction.ASC)
.partial(of(new org.bson.Document("qty", new org.bson.Document("$gte", 10))));
@@ -144,7 +146,7 @@ public void shouldApplyPartialDBOFilterCorrectly() {
}
@Test // DATAMONGO-1682, DATAMONGO-2198
- public void shouldFavorExplicitMappingHintViaClass() {
+ void shouldFavorExplicitMappingHintViaClass() {
IndexDefinition id = new Index().named("partial-with-inheritance").on("k3y", Direction.ASC)
.partial(of(where("age").gte(10)));
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsUnitTests.java
index 195952dbec..a866ae3ede 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsUnitTests.java
@@ -20,7 +20,11 @@
import reactor.core.publisher.Mono;
+import java.util.List;
+import java.util.function.BiConsumer;
+
import org.bson.Document;
+import org.bson.conversions.Bson;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -28,6 +32,7 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.reactivestreams.Publisher;
+
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@@ -38,13 +43,18 @@
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Collation;
+import com.mongodb.client.model.CreateIndexOptions;
+import com.mongodb.client.model.IndexModel;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
/**
+ * Unit tests for {@link DefaultReactiveIndexOperations}.
+ *
* @author Christoph Strobl
* @author Mathieu Ouellet
+ * @author Mark Paluch
*/
@ExtendWith(MockitoExtension.class)
public class DefaultReactiveIndexOperationsUnitTests {
@@ -66,7 +76,7 @@ void setUp() {
when(factory.getMongoDatabase()).thenReturn(Mono.just(db));
when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator);
when(db.getCollection(any(), any(Class.class))).thenReturn(collection);
- when(collection.createIndex(any(), any(IndexOptions.class))).thenReturn(publisher);
+ when(collection.createIndexes(anyList(), any(CreateIndexOptions.class))).thenReturn(publisher);
this.mappingContext = new MongoMappingContext();
this.converter = spy(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext));
@@ -78,10 +88,7 @@ void ensureIndexDoesNotSetCollectionIfNoDefaultDefined() {
indexOpsFor(Jedi.class).ensureIndex(new Index("firstname", Direction.DESC)).subscribe();
- ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class);
- verify(collection).createIndex(any(), options.capture());
-
- assertThat(options.getValue().getCollation()).isNull();
+ verifyCreateIndex((keys, options) -> assertThat(options.getCollation()).isNull());
}
@Test // DATAMONGO-1854
@@ -89,11 +96,8 @@ void ensureIndexUsesDefaultCollationIfNoneDefinedInOptions() {
indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC)).subscribe();
- ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class);
- verify(collection).createIndex(any(), options.capture());
-
- assertThat(options.getValue().getCollation())
- .isEqualTo(com.mongodb.client.model.Collation.builder().locale("de_AT").build());
+ verifyCreateIndex((keys, options) -> assertThat(options.getCollation())
+ .isEqualTo(com.mongodb.client.model.Collation.builder().locale("de_AT").build()));
}
@Test // DATAMONGO-1854
@@ -102,11 +106,19 @@ void ensureIndexDoesNotUseDefaultCollationIfExplicitlySpecifiedInTheIndex() {
indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC).collation(Collation.of("en_US")))
.subscribe();
- ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class);
- verify(collection).createIndex(any(), options.capture());
- assertThat(options.getValue().getCollation())
- .isEqualTo(com.mongodb.client.model.Collation.builder().locale("en_US").build());
+ verifyCreateIndex((keys, options) -> assertThat(options.getCollation())
+ .isEqualTo(com.mongodb.client.model.Collation.builder().locale("en_US").build()));
+ }
+
+ private void verifyCreateIndex(BiConsumer consumer) {
+
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
+
+ verify(collection).createIndexes(captor.capture(), any());
+
+ IndexModel indexModel = captor.getValue().get(0);
+ consumer.accept(indexModel.getKeys(), indexModel.getOptions());
}
private DefaultReactiveIndexOperations indexOpsFor(Class> type) {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateIndexTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateIndexTests.java
index 491ebd2cd2..40225d53b3 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateIndexTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateIndexTests.java
@@ -15,12 +15,9 @@
*/
package org.springframework.data.mongodb.core;
-import static org.assertj.core.data.Index.*;
import static org.assertj.core.data.Index.atIndex;
import static org.springframework.data.mongodb.test.util.Assertions.*;
-import org.junit.jupiter.api.RepeatedTest;
-import org.junitpioneer.jupiter.RetryingTest;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
@@ -32,6 +29,7 @@
import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
+import org.junitpioneer.jupiter.RetryingTest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.annotation.Id;
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 71837d7803..e3b76afdc8 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
@@ -20,6 +20,7 @@
import java.util.Collections;
import java.util.Date;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
@@ -46,6 +47,8 @@
import com.mongodb.MongoException;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.CreateIndexOptions;
+import com.mongodb.client.model.IndexModel;
import com.mongodb.client.model.IndexOptions;
/**
@@ -67,16 +70,14 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
private @Mock MongoCollection collection;
private MongoTemplate mongoTemplate;
- private ArgumentCaptor keysCaptor;
- private ArgumentCaptor optionsCaptor;
+ private ArgumentCaptor> indexModelCaptor;
private ArgumentCaptor collectionCaptor;
@BeforeEach
void setUp() {
- keysCaptor = ArgumentCaptor.forClass(org.bson.Document.class);
- optionsCaptor = ArgumentCaptor.forClass(IndexOptions.class);
collectionCaptor = ArgumentCaptor.forClass(String.class);
+ indexModelCaptor = ArgumentCaptor.forClass(List.class);
when(factory.getMongoDatabase()).thenReturn(db);
when(factory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator());
@@ -84,8 +85,7 @@ void setUp() {
.thenReturn((MongoCollection) collection);
mongoTemplate = new MongoTemplate(factory);
-
- when(collection.createIndex(keysCaptor.capture(), optionsCaptor.capture())).thenReturn("OK");
+ when(collection.createIndexes(indexModelCaptor.capture(), any(CreateIndexOptions.class))).thenReturn(List.of("OK"));
}
@Test
@@ -95,10 +95,12 @@ void buildsIndexDefinitionUsingFieldName() {
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
- assertThat(keysCaptor.getValue()).isNotNull().containsKey("fieldname");
- assertThat(optionsCaptor.getValue().getName()).isEqualTo("indexName");
- assertThat(optionsCaptor.getValue().isBackground()).isFalse();
- assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isNull();
+ IndexModel indexModel = indexModelCaptor.getValue().get(0);
+
+ assertThat(indexModel.getKeys().toBsonDocument()).isNotNull().containsKey("fieldname");
+ assertThat(indexModel.getOptions().getName()).isEqualTo("indexName");
+ assertThat(indexModel.getOptions().isBackground()).isFalse();
+ assertThat(indexModel.getOptions().getExpireAfter(TimeUnit.SECONDS)).isNull();
}
@Test
@@ -135,10 +137,12 @@ void triggersBackgroundIndexingIfConfigured() {
MongoMappingContext mappingContext = prepareMappingContext(AnotherPerson.class);
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
- assertThat(keysCaptor.getValue()).isNotNull().containsKey("lastname");
- assertThat(optionsCaptor.getValue().getName()).isEqualTo("lastname");
- assertThat(optionsCaptor.getValue().isBackground()).isTrue();
- assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isNull();
+ IndexModel indexModel = indexModelCaptor.getValue().get(0);
+
+ assertThat(indexModel.getKeys().toBsonDocument()).isNotNull().containsKey("lastname");
+ assertThat(indexModel.getOptions().getName()).isEqualTo("lastname");
+ assertThat(indexModel.getOptions().isBackground()).isTrue();
+ assertThat(indexModel.getOptions().getExpireAfter(TimeUnit.SECONDS)).isNull();
}
@Test // DATAMONGO-544
@@ -147,8 +151,10 @@ void expireAfterSecondsIfConfigured() {
MongoMappingContext mappingContext = prepareMappingContext(Milk.class);
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
- assertThat(keysCaptor.getValue()).isNotNull().containsKey("expiry");
- assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(60);
+ IndexModel indexModel = indexModelCaptor.getValue().get(0);
+
+ assertThat(indexModel.getKeys().toBsonDocument()).isNotNull().containsKey("expiry");
+ assertThat(indexModel.getOptions().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(60);
}
@Test // DATAMONGO-899
@@ -157,9 +163,11 @@ void createsNotNestedGeoSpatialIndexCorrectly() {
MongoMappingContext mappingContext = prepareMappingContext(Wrapper.class);
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
- assertThat(keysCaptor.getValue()).isEqualTo(new org.bson.Document("company.address.location", "2d"));
+ IndexModel indexModel = indexModelCaptor.getValue().get(0);
+
+ assertThat(indexModel.getKeys()).isEqualTo(new org.bson.Document("company.address.location", "2d"));
- IndexOptions opts = optionsCaptor.getValue();
+ IndexOptions opts = indexModel.getOptions();
assertThat(opts.getName()).isEqualTo("company.address.location");
assertThat(opts.getMin()).isCloseTo(-180d, offset(0d));
assertThat(opts.getMax()).isCloseTo(180d, offset(0d));
@@ -172,8 +180,10 @@ void autoGeneratedIndexNameShouldGenerateNoName() {
MongoMappingContext mappingContext = prepareMappingContext(EntityWithGeneratedIndexName.class);
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
- assertThat(keysCaptor.getValue()).doesNotContainKey("name").containsKey("lastname");
- assertThat(optionsCaptor.getValue().getName()).isNull();
+ IndexModel indexModel = indexModelCaptor.getValue().get(0);
+
+ assertThat(indexModel.getKeys().toBsonDocument()).doesNotContainKey("name").containsKey("lastname");
+ assertThat(indexModel.getOptions().getName()).isNull();
}
@Test // DATAMONGO-367
@@ -203,8 +213,8 @@ void indexCreationShouldNotCreateNewCollectionForNestedIndexStructures() {
@Test // DATAMONGO-1125
void createIndexShouldUsePersistenceExceptionTranslatorForNonDataIntegrityConcerns() {
- doThrow(new MongoException(6, "HostUnreachable")).when(collection).createIndex(any(org.bson.Document.class),
- any(IndexOptions.class));
+ doThrow(new MongoException(6, "HostUnreachable")).when(collection).createIndexes(anyList(),
+ any(CreateIndexOptions.class));
MongoMappingContext mappingContext = prepareMappingContext(Person.class);
@@ -215,8 +225,7 @@ void createIndexShouldUsePersistenceExceptionTranslatorForNonDataIntegrityConcer
@Test // DATAMONGO-1125
void createIndexShouldNotConvertUnknownExceptionTypes() {
- doThrow(new ClassCastException("o_O")).when(collection).createIndex(any(org.bson.Document.class),
- any(IndexOptions.class));
+ doThrow(new ClassCastException("o_O")).when(collection).createIndexes(anyList(), any(CreateIndexOptions.class));
MongoMappingContext mappingContext = prepareMappingContext(Person.class);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/ReactiveMongoPersistentEntityIndexCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/ReactiveMongoPersistentEntityIndexCreatorUnitTests.java
index 3ff07ecf7c..eb8a9d4891 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/ReactiveMongoPersistentEntityIndexCreatorUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/ReactiveMongoPersistentEntityIndexCreatorUnitTests.java
@@ -22,6 +22,7 @@
import reactor.test.StepVerifier;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
@@ -42,6 +43,8 @@
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import com.mongodb.MongoException;
+import com.mongodb.client.model.CreateIndexOptions;
+import com.mongodb.client.model.IndexModel;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
@@ -62,6 +65,7 @@ public class ReactiveMongoPersistentEntityIndexCreatorUnitTests {
@Mock MongoDatabase db;
@Mock MongoCollection collection;
+ private ArgumentCaptor> indexModelCaptor;
private ArgumentCaptor keysCaptor;
private ArgumentCaptor optionsCaptor;
private ArgumentCaptor collectionCaptor;
@@ -79,8 +83,11 @@ void setUp() {
keysCaptor = ArgumentCaptor.forClass(org.bson.Document.class);
optionsCaptor = ArgumentCaptor.forClass(IndexOptions.class);
collectionCaptor = ArgumentCaptor.forClass(String.class);
+ indexModelCaptor = ArgumentCaptor.forClass(List.class);
when(collection.createIndex(keysCaptor.capture(), optionsCaptor.capture())).thenReturn(Mono.just("OK"));
+ when(collection.createIndexes(indexModelCaptor.capture(), any(CreateIndexOptions.class)))
+ .thenReturn(Mono.just("OK"));
}
@Test // DATAMONGO-1928
@@ -94,16 +101,18 @@ void buildsIndexDefinitionUsingFieldName() {
publisher.as(StepVerifier::create).verifyComplete();
- assertThat(keysCaptor.getValue()).isNotNull().containsKey("fieldname");
- assertThat(optionsCaptor.getValue().getName()).isEqualTo("indexName");
- assertThat(optionsCaptor.getValue().isBackground()).isFalse();
- assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isNull();
+ IndexModel indexModel = indexModelCaptor.getValue().get(0);
+
+ assertThat(indexModel.getKeys().toBsonDocument()).isNotNull().containsKey("fieldname");
+ assertThat(indexModel.getOptions().getName()).isEqualTo("indexName");
+ assertThat(indexModel.getOptions().isBackground()).isFalse();
+ assertThat(indexModel.getOptions().getExpireAfter(TimeUnit.SECONDS)).isNull();
}
@Test // DATAMONGO-1928
void createIndexShouldUsePersistenceExceptionTranslatorForNonDataIntegrityConcerns() {
- when(collection.createIndex(any(org.bson.Document.class), any(IndexOptions.class)))
+ when(collection.createIndexes(indexModelCaptor.capture(), any(CreateIndexOptions.class)))
.thenReturn(Mono.error(new MongoException(6, "HostUnreachable")));
MongoMappingContext mappingContext = prepareMappingContext(Person.class);
@@ -116,7 +125,7 @@ void createIndexShouldUsePersistenceExceptionTranslatorForNonDataIntegrityConcer
@Test // DATAMONGO-1928
void createIndexShouldNotConvertUnknownExceptionTypes() {
- when(collection.createIndex(any(org.bson.Document.class), any(IndexOptions.class)))
+ when(collection.createIndexes(any(List.class), any(CreateIndexOptions.class)))
.thenReturn(Mono.error(new ClassCastException("o_O")));
MongoMappingContext mappingContext = prepareMappingContext(Person.class);