diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java index 49eb75317..84fe0260b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java @@ -35,6 +35,8 @@ import org.springframework.util.NumberUtils; import com.mongodb.MongoException; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; +import com.mongodb.client.model.CreateIndexOptions; +import com.mongodb.client.model.IndexModel; import com.mongodb.client.model.IndexOptions; /** @@ -53,6 +55,7 @@ public class DefaultIndexOperations implements IndexOperations { private final String collectionName; private final QueryMapper mapper; private final @Nullable Class type; + private final CreateIndexOptions createIndexOptions; private final MongoOperations mongoOperations; @@ -92,6 +95,7 @@ public class DefaultIndexOperations implements IndexOperations { this.collectionName = collectionName; this.mapper = queryMapper; this.type = type; + this.createIndexOptions = new CreateIndexOptions(); this.mongoOperations = new MongoTemplate(mongoDbFactory); } @@ -104,14 +108,30 @@ public class DefaultIndexOperations implements IndexOperations { * @since 2.1 */ public DefaultIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class type) { + this(mongoOperations, collectionName, type, new CreateIndexOptions()); + } + + /** + * Creates a new {@link DefaultIndexOperations}. + * + * @param mongoOperations must not be {@literal null}. + * @param collectionName must not be {@literal null} or empty. + * @param type can be {@literal null}. + * @param createIndexOptions must not be {@literal null}. + * @since 4.5 + */ + public DefaultIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class type, + CreateIndexOptions createIndexOptions) { Assert.notNull(mongoOperations, "MongoOperations must not be null"); Assert.hasText(collectionName, "Collection name must not be null or empty"); + Assert.notNull(createIndexOptions, "CreateIndexOptions must not be null"); this.mongoOperations = mongoOperations; this.mapper = new QueryMapper(mongoOperations.getConverter()); this.collectionName = collectionName; this.type = type; + this.createIndexOptions = createIndexOptions; } @Override @@ -128,7 +148,8 @@ public class DefaultIndexOperations implements IndexOperations { indexOptions = addDefaultCollationIfRequired(indexOptions, entity); Document mappedKeys = mapper.getMappedSort(indexDefinition.getIndexKeys(), entity); - return collection.createIndex(mappedKeys, indexOptions); + IndexModel indexModel = new IndexModel(mappedKeys, indexOptions); + return collection.createIndexes(List.of(indexModel), createIndexOptions).get(0); }); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java index b3baf3e89..a7dd2b332 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java @@ -19,6 +19,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; +import java.util.List; import org.bson.Document; import org.jspecify.annotations.Nullable; @@ -31,6 +32,8 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; 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; /** @@ -48,6 +51,7 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { private final String collectionName; private final QueryMapper queryMapper; private final @Nullable Class type; + private final CreateIndexOptions createIndexOptions; /** * Creates a new {@link DefaultReactiveIndexOperations}. @@ -71,15 +75,32 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { */ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName, QueryMapper queryMapper, @Nullable Class type) { + this(mongoOperations, collectionName, queryMapper, type, new CreateIndexOptions()); + } + + /** + * Creates a new {@link DefaultReactiveIndexOperations}. + * + * @param mongoOperations must not be {@literal null}. + * @param collectionName must not be {@literal null}. + * @param queryMapper must not be {@literal null}. + * @param type used for mapping potential partial index filter expression, must not be {@literal null}. + * @param createIndexOptions must not be {@literal null}. + * @since 4.5 + */ + public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName, + QueryMapper queryMapper, @Nullable Class type, CreateIndexOptions createIndexOptions) { Assert.notNull(mongoOperations, "ReactiveMongoOperations must not be null"); Assert.notNull(collectionName, "Collection must not be null"); Assert.notNull(queryMapper, "QueryMapper must not be null"); + Assert.notNull(createIndexOptions, "CreateIndexOptions must not be null"); this.mongoOperations = mongoOperations; this.collectionName = collectionName; this.queryMapper = queryMapper; this.type = type; + this.createIndexOptions = createIndexOptions; } @Override @@ -95,7 +116,8 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { indexOptions = addPartialFilterIfPresent(indexOptions, indexDefinition.getIndexOptions(), entity); indexOptions = addDefaultCollationIfRequired(indexOptions, entity); - return collection.createIndex(indexDefinition.getIndexKeys(), indexOptions); + IndexModel indexModel = new IndexModel(indexDefinition.getIndexKeys(), indexOptions); + return collection.createIndexes(List.of(indexModel), createIndexOptions); }).next(); } 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 9e66e259b..342a28e2d 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 @@ -16,8 +16,11 @@ package org.springframework.data.mongodb.core; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.util.List; + import org.bson.Document; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,6 +40,8 @@ import org.springframework.data.mongodb.core.query.Collation; 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 +69,7 @@ public class DefaultIndexOperationsUnitTests { 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 +81,9 @@ public class DefaultIndexOperationsUnitTests { indexOpsFor(Jedi.class).ensureIndex(new Index("name", Direction.DESC)); - verify(collection).createIndex(eq(new Document("firstname", -1)), any()); + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(collection).createIndexes(captor.capture(), any(CreateIndexOptions.class)); + assertThat(captor.getValue().get(0).getKeys()).isEqualTo(new Document("firstname", -1)); } @Test // DATAMONGO-1854 @@ -84,10 +91,10 @@ public class DefaultIndexOperationsUnitTests { indexOpsFor(Jedi.class).ensureIndex(new Index("firstname", Direction.DESC)); - ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); - verify(collection).createIndex(any(), options.capture()); + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(collection).createIndexes(captor.capture(), any(CreateIndexOptions.class)); - assertThat(options.getValue().getCollation()).isNull(); + assertThat(captor.getValue().get(0).getOptions().getCollation()).isNull(); } @Test // DATAMONGO-1854 @@ -95,10 +102,10 @@ public class DefaultIndexOperationsUnitTests { indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC)); - ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); - verify(collection).createIndex(any(), options.capture()); + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(collection).createIndexes(captor.capture(), any(CreateIndexOptions.class)); - assertThat(options.getValue().getCollation()) + assertThat(captor.getValue().get(0).getOptions().getCollation()) .isEqualTo(com.mongodb.client.model.Collation.builder().locale("de_AT").build()); } @@ -107,10 +114,10 @@ public class DefaultIndexOperationsUnitTests { 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()); + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(collection).createIndexes(captor.capture(), any(CreateIndexOptions.class)); - assertThat(options.getValue().getCollation()) + assertThat(captor.getValue().get(0).getOptions().getCollation()) .isEqualTo(com.mongodb.client.model.Collation.builder().locale("en_US").build()); } @@ -119,7 +126,9 @@ public class DefaultIndexOperationsUnitTests { indexOpsFor(Jedi.class).ensureIndex(HashedIndex.hashed("name")); - verify(collection).createIndex(eq(new Document("firstname", "hashed")), any()); + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(collection).createIndexes(captor.capture(), any(CreateIndexOptions.class)); + assertThat(captor.getValue().get(0).getKeys()).isEqualTo(new Document("firstname", "hashed")); } @Test // GH-4698 @@ -131,6 +140,22 @@ public class DefaultIndexOperationsUnitTests { verify(db).getCollection(eq("foo"), any(Class.class)); } + @Test // GH-4422 + void shouldPassCreateIndexOptionsToDriver() { + + CreateIndexOptions createIndexOptions = new CreateIndexOptions() + .commitQuorum(com.mongodb.CreateIndexCommitQuorum.MAJORITY); + + DefaultIndexOperations operations = new DefaultIndexOperations(template, template.getCollectionName(Jedi.class), + Jedi.class, createIndexOptions); + + operations.createIndex(new Index("name", Direction.DESC)); + + ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(CreateIndexOptions.class); + verify(collection).createIndexes(anyList(), optionsCaptor.capture()); + assertThat(optionsCaptor.getValue()).isSameAs(createIndexOptions); + } + 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/DefaultReactiveIndexOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsUnitTests.java index 195952dbe..fbe1cdaa5 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 @@ -16,10 +16,14 @@ package org.springframework.data.mongodb.core; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.List; + import org.bson.Document; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,6 +42,8 @@ import org.springframework.data.mongodb.core.mapping.Field; 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; @@ -66,7 +72,7 @@ public class DefaultReactiveIndexOperationsUnitTests { 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(Flux.just("OK")); this.mappingContext = new MongoMappingContext(); this.converter = spy(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)); @@ -78,10 +84,10 @@ public class DefaultReactiveIndexOperationsUnitTests { indexOpsFor(Jedi.class).ensureIndex(new Index("firstname", Direction.DESC)).subscribe(); - ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); - verify(collection).createIndex(any(), options.capture()); + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(collection).createIndexes(captor.capture(), any(CreateIndexOptions.class)); - assertThat(options.getValue().getCollation()).isNull(); + assertThat(captor.getValue().get(0).getOptions().getCollation()).isNull(); } @Test // DATAMONGO-1854 @@ -89,10 +95,10 @@ public class DefaultReactiveIndexOperationsUnitTests { indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC)).subscribe(); - ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); - verify(collection).createIndex(any(), options.capture()); + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(collection).createIndexes(captor.capture(), any(CreateIndexOptions.class)); - assertThat(options.getValue().getCollation()) + assertThat(captor.getValue().get(0).getOptions().getCollation()) .isEqualTo(com.mongodb.client.model.Collation.builder().locale("de_AT").build()); } @@ -102,13 +108,30 @@ public class DefaultReactiveIndexOperationsUnitTests { 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()); + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(collection).createIndexes(captor.capture(), any(CreateIndexOptions.class)); - assertThat(options.getValue().getCollation()) + assertThat(captor.getValue().get(0).getOptions().getCollation()) .isEqualTo(com.mongodb.client.model.Collation.builder().locale("en_US").build()); } + @Test // GH-4422 + void shouldPassCreateIndexOptionsToDriver() { + + CreateIndexOptions createIndexOptions = new CreateIndexOptions() + .commitQuorum(com.mongodb.CreateIndexCommitQuorum.MAJORITY); + + DefaultReactiveIndexOperations operations = new DefaultReactiveIndexOperations(template, + template.getCollectionName(Jedi.class), new QueryMapper(template.getConverter()), Jedi.class, + createIndexOptions); + + operations.ensureIndex(new Index("firstname", Direction.DESC)).subscribe(); + + ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(CreateIndexOptions.class); + verify(collection).createIndexes(anyList(), optionsCaptor.capture()); + assertThat(optionsCaptor.getValue()).isSameAs(createIndexOptions); + } + private DefaultReactiveIndexOperations indexOpsFor(Class type) { return new DefaultReactiveIndexOperations(template, template.getCollectionName(type), new QueryMapper(template.getConverter()), type);