diff --git a/pom.xml b/pom.xml index 3e4f4c3130..58b6fb7001 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.1.0-SNAPSHOT + 5.1.0-4422-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index cab02fe276..72ced03d84 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.1.0-SNAPSHOT + 5.1.0-4422-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 0a758111af..be8a8d72ae 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.1.0-SNAPSHOT + 5.1.0-4422-SNAPSHOT ../pom.xml 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 49eb75317f..ab75b1dc89 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 @@ -16,26 +16,18 @@ package org.springframework.data.mongodb.core; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import org.bson.Document; import org.jspecify.annotations.Nullable; -import org.springframework.dao.DataAccessException; + import org.springframework.data.mongodb.MongoDatabaseFactory; -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.index.IndexInfo; import org.springframework.data.mongodb.core.index.IndexOperations; -import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.CollectionName; import org.springframework.util.Assert; -import org.springframework.util.NumberUtils; - -import com.mongodb.MongoException; -import com.mongodb.client.MongoCollection; -import com.mongodb.client.MongoCursor; -import com.mongodb.client.model.IndexOptions; /** * Default implementation of {@link IndexOperations}. @@ -46,24 +38,18 @@ * @author Christoph Strobl * @author Mark Paluch */ -public class DefaultIndexOperations implements IndexOperations { - - private static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression"; - - private final String collectionName; - private final QueryMapper mapper; - private final @Nullable Class type; +public class DefaultIndexOperations extends IndexOperationsSupport implements IndexOperations { private final MongoOperations mongoOperations; /** - * Creates a new {@link DefaultIndexOperations}. + * Creates a new {@code DefaultIndexOperations}. * * @param mongoDbFactory must not be {@literal null}. * @param collectionName must not be {@literal null}. * @param queryMapper must not be {@literal null}. * @deprecated since 2.1. Please use - * {@link DefaultIndexOperations#DefaultIndexOperations(MongoOperations, String, Class)}. + * {@code DefaultIndexOperations#DefaultIndexOperations(MongoOperations, String, Class)}. */ @Deprecated public DefaultIndexOperations(MongoDatabaseFactory mongoDbFactory, String collectionName, QueryMapper queryMapper) { @@ -71,7 +57,7 @@ public DefaultIndexOperations(MongoDatabaseFactory mongoDbFactory, String collec } /** - * Creates a new {@link DefaultIndexOperations}. + * Creates a new {@code DefaultIndexOperations}. * * @param mongoDbFactory must not be {@literal null}. * @param collectionName must not be {@literal null}. @@ -79,24 +65,16 @@ public DefaultIndexOperations(MongoDatabaseFactory mongoDbFactory, String collec * @param type Type used for mapping potential partial index filter expression. Can be {@literal null}. * @since 1.10 * @deprecated since 2.1. Please use - * {@link DefaultIndexOperations#DefaultIndexOperations(MongoOperations, String, Class)}. + * {@code DefaultIndexOperations#DefaultIndexOperations(MongoOperations, String, Class)}. */ @Deprecated public DefaultIndexOperations(MongoDatabaseFactory mongoDbFactory, String collectionName, QueryMapper queryMapper, @Nullable Class type) { - - Assert.notNull(mongoDbFactory, "MongoDbFactory must not be null"); - Assert.notNull(collectionName, "Collection name can not be null"); - Assert.notNull(queryMapper, "QueryMapper must not be null"); - - this.collectionName = collectionName; - this.mapper = queryMapper; - this.type = type; - this.mongoOperations = new MongoTemplate(mongoDbFactory); + this(new MongoTemplate(mongoDbFactory), collectionName, queryMapper, type); } /** - * Creates a new {@link DefaultIndexOperations}. + * Creates a new {@code DefaultIndexOperations}. * * @param mongoOperations must not be {@literal null}. * @param collectionName must not be {@literal null} or empty. @@ -104,51 +82,50 @@ public DefaultIndexOperations(MongoDatabaseFactory mongoDbFactory, String collec * @since 2.1 */ public DefaultIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class type) { + this(mongoOperations, CollectionName.just(collectionName), new QueryMapper(mongoOperations.getConverter()), type); + } - Assert.notNull(mongoOperations, "MongoOperations must not be null"); - Assert.hasText(collectionName, "Collection name must not be null or empty"); + /** + * Creates a new {@code DefaultIndexOperations}. + * + * @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. + * @since 5.1 + */ + public DefaultIndexOperations(MongoOperations mongoOperations, String collectionName, QueryMapper queryMapper, + @Nullable Class type) { + this(mongoOperations, CollectionName.just(collectionName), queryMapper, type); + } + + /** + * Creates a new {@code DefaultIndexOperations}. + * + * @param mongoOperations must not be {@literal null}. + * @param collectionName must not be {@literal null} or empty. + * @param type can be {@literal null}. + * @since 5.1 + */ + protected DefaultIndexOperations(MongoOperations mongoOperations, CollectionName collectionName, + QueryMapper queryMapper, @Nullable Class type) { + + super(collectionName, queryMapper, type); + Assert.notNull(mongoOperations, "MongoOperations must not be null"); this.mongoOperations = mongoOperations; - this.mapper = new QueryMapper(mongoOperations.getConverter()); - this.collectionName = collectionName; - this.type = type; } @Override - @SuppressWarnings("NullAway") public String createIndex(IndexDefinition indexDefinition) { return execute(collection -> { - MongoPersistentEntity entity = lookupPersistentEntity(type, collectionName); - - IndexOptions indexOptions = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition); - - indexOptions = addPartialFilterIfPresent(indexOptions, indexDefinition.getIndexOptions(), entity); - indexOptions = addDefaultCollationIfRequired(indexOptions, entity); - - Document mappedKeys = mapper.getMappedSort(indexDefinition.getIndexKeys(), entity); - return collection.createIndex(mappedKeys, indexOptions); + CreateIndexCommand command = toCreateIndexCommand(indexDefinition); + return collection.createIndexes(command.toIndexModels(), command.createIndexOptions()).get(0); }); } - private @Nullable MongoPersistentEntity lookupPersistentEntity(@Nullable Class entityType, String collection) { - - if (entityType != null) { - return mapper.getMappingContext().getRequiredPersistentEntity(entityType); - } - - Collection> entities = mapper.getMappingContext().getPersistentEntities(); - - for (MongoPersistentEntity entity : entities) { - if (entity.getCollection().equals(collection)) { - return entity; - } - } - - return null; - } - @Override public void dropIndex(String name) { @@ -156,23 +133,16 @@ public void dropIndex(String name) { collection.dropIndex(name); return null; }); - } @Override @SuppressWarnings("NullAway") public void alterIndex(String name, org.springframework.data.mongodb.core.index.IndexOptions options) { - Document indexOptions = new Document("name", name); - indexOptions.putAll(options.toDocument()); - Document result = mongoOperations - .execute(db -> db.runCommand(new Document("collMod", collectionName).append("index", indexOptions))); + .execute(db -> db.runCommand(alterIndexCommand(name, options), Document.class)); - if (NumberUtils.convertNumberToTargetClass(result.get("ok", (Number) 0), Integer.class) != 1) { - throw new UncategorizedMongoDbException( - "Index '%s' could not be modified. Response was %s".formatted(name, result.toJson()), null); - } + validateAlterIndexResponse(name, result); } @Override @@ -181,62 +151,15 @@ public void dropAllIndexes() { } @Override - @SuppressWarnings("NullAway") public List getIndexInfo() { - - return execute(new CollectionCallback>() { - - public List doInCollection(MongoCollection collection) - throws MongoException, DataAccessException { - - MongoCursor cursor = collection.listIndexes(Document.class).iterator(); - return getIndexData(cursor); - } - - private List getIndexData(MongoCursor cursor) { - - int available = cursor.available(); - List indexInfoList = available > 0 ? new ArrayList<>(available) : new ArrayList<>(); - - while (cursor.hasNext()) { - - Document ix = cursor.next(); - IndexInfo indexInfo = IndexConverters.documentToIndexInfoConverter().convert(ix); - indexInfoList.add(indexInfo); - } - - return indexInfoList; - } - }); - } - - public @Nullable T execute(CollectionCallback callback) { - - Assert.notNull(callback, "CollectionCallback must not be null"); - - return mongoOperations.execute(collectionName, callback); - } - - 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( - mapper.getMappedSort((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity)); + return execute( + collection -> collection.listIndexes(Document.class).map(IndexInfo::indexInfoOf).into(new ArrayList<>())); } @SuppressWarnings("NullAway") - private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops, - @Nullable MongoPersistentEntity entity) { - - if (ops.getCollation() != null || entity == null || !entity.hasCollation()) { - return ops; - } + public T execute(CollectionCallback callback) { - return ops.collation(entity.getCollation().toMongoCollation()); + Assert.notNull(callback, "CollectionCallback must not be null"); + return mongoOperations.execute(getCollectionName(), callback); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java index 0005f5e19f..7eb4cfdae9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java @@ -44,7 +44,7 @@ class DefaultIndexOperationsProvider implements IndexOperationsProvider { } @Override - public IndexOperations indexOps(String collectionName, @Nullable Class type) { - return new DefaultIndexOperations(mongoDbFactory, collectionName, mapper, type); + public IndexOperations indexOps(String collectionName, @Nullable Class entityClass) { + return new DefaultIndexOperations(mongoDbFactory, collectionName, mapper, entityClass); } } 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 b3baf3e897..d7ce62dd33 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 @@ -18,20 +18,15 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.Collection; - import org.bson.Document; import org.jspecify.annotations.Nullable; -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.index.IndexInfo; import org.springframework.data.mongodb.core.index.ReactiveIndexOperations; -import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.CollectionName; import org.springframework.util.Assert; -import org.springframework.util.NumberUtils; - -import com.mongodb.client.model.IndexOptions; /** * Default implementation of {@link ReactiveIndexOperations}. @@ -40,17 +35,12 @@ * @author Christoph Strobl * @since 2.0 */ -public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { - - private static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression"; +public class DefaultReactiveIndexOperations extends IndexOperationsSupport implements ReactiveIndexOperations { private final ReactiveMongoOperations mongoOperations; - private final String collectionName; - private final QueryMapper queryMapper; - private final @Nullable Class type; /** - * Creates a new {@link DefaultReactiveIndexOperations}. + * Creates a new {@code DefaultReactiveIndexOperations}. * * @param mongoOperations must not be {@literal null}. * @param collectionName must not be {@literal null}. @@ -62,41 +52,43 @@ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, S } /** - * Creates a new {@link DefaultReactiveIndexOperations}. + * Creates a new {@code 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 type used for mapping potential partial index filter expression. */ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName, QueryMapper queryMapper, @Nullable Class type) { + this(mongoOperations, CollectionName.just(collectionName), queryMapper, type); + } - Assert.notNull(mongoOperations, "ReactiveMongoOperations must not be null"); - Assert.notNull(collectionName, "Collection must not be null"); - Assert.notNull(queryMapper, "QueryMapper must not be null"); + /** + * Creates a new {@code 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. + */ + protected DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, CollectionName collectionName, + QueryMapper queryMapper, @Nullable Class type) { + + super(collectionName, queryMapper, type); + Assert.notNull(mongoOperations, "ReactiveMongoOperations must not be null"); this.mongoOperations = mongoOperations; - this.collectionName = collectionName; - this.queryMapper = queryMapper; - this.type = type; } @Override @SuppressWarnings("NullAway") public Mono createIndex(IndexDefinition indexDefinition) { - return mongoOperations.execute(collectionName, collection -> { - - MongoPersistentEntity entity = getConfiguredEntity(); - - IndexOptions indexOptions = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition); - - indexOptions = addPartialFilterIfPresent(indexOptions, indexDefinition.getIndexOptions(), entity); - indexOptions = addDefaultCollationIfRequired(indexOptions, entity); - - return collection.createIndex(indexDefinition.getIndexKeys(), indexOptions); + return mongoOperations.execute(getCollectionName(), collection -> { + CreateIndexCommand command = toCreateIndexCommand(indexDefinition); + return collection.createIndexes(command.toIndexModels(), command.createIndexOptions()); }).next(); } @@ -104,32 +96,16 @@ public Mono createIndex(IndexDefinition indexDefinition) { public Mono alterIndex(String name, org.springframework.data.mongodb.core.index.IndexOptions options) { return mongoOperations.execute(db -> { - Document indexOptions = new Document("name", name); - indexOptions.putAll(options.toDocument()); - - return Flux.from(db.runCommand(new Document("collMod", collectionName).append("index", indexOptions))) + return Flux.from(db.runCommand(alterIndexCommand(name, options))) .doOnNext(result -> { - if (NumberUtils.convertNumberToTargetClass(result.get("ok", (Number) 0), Integer.class) != 1) { - throw new UncategorizedMongoDbException( - "Index '%s' could not be modified. Response was %s".formatted(name, result.toJson()), null); - } + validateAlterIndexResponse(name, result); }); }).then(); } - private @Nullable MongoPersistentEntity lookupPersistentEntity(String collection) { - - Collection> entities = queryMapper.getMappingContext().getPersistentEntities(); - - return entities.stream() // - .filter(entity -> entity.getCollection().equals(collection)) // - .findFirst() // - .orElse(null); - } - @Override public Mono dropIndex(String name) { - return mongoOperations.execute(collectionName, collection -> collection.dropIndex(name)).then(); + return mongoOperations.execute(getCollectionName(), collection -> collection.dropIndex(name)).then(); } @Override @@ -139,39 +115,8 @@ public Mono dropAllIndexes() { @Override public Flux getIndexInfo() { - - return mongoOperations.execute(collectionName, collection -> collection.listIndexes(Document.class)) // - .map(IndexConverters.documentToIndexInfoConverter()::convert); - } - - private @Nullable MongoPersistentEntity getConfiguredEntity() { - - if (type != null) { - return queryMapper.getMappingContext().getRequiredPersistentEntity(type); - } - return lookupPersistentEntity(collectionName); - } - - 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)); + return mongoOperations.execute(getCollectionName(), collection -> collection.listIndexes(Document.class)) // + .map(IndexInfo::indexInfoOf); } - @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()); - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index 9a8d7a1dcd..ff634a01e2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -19,9 +19,9 @@ import org.bson.Document; import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.core.index.IndexDefinition; -import org.springframework.data.mongodb.core.index.IndexInfo; import org.springframework.util.ObjectUtils; import com.mongodb.client.model.Collation; @@ -36,92 +36,72 @@ */ abstract class IndexConverters { - private static final Converter DEFINITION_TO_MONGO_INDEX_OPTIONS; - private static final Converter DOCUMENT_INDEX_INFO; - - static { - - DEFINITION_TO_MONGO_INDEX_OPTIONS = getIndexDefinitionIndexOptionsConverter(); - DOCUMENT_INDEX_INFO = getDocumentIndexInfoConverter(); - } - private IndexConverters() { } - static Converter indexDefinitionToIndexOptionsConverter() { - return DEFINITION_TO_MONGO_INDEX_OPTIONS; - } + static IndexOptions toIndexOptions(IndexDefinition indexDefinition) { - static Converter documentToIndexInfoConverter() { - return DOCUMENT_INDEX_INFO; - } - - private static Converter getIndexDefinitionIndexOptionsConverter() { - - return indexDefinition -> { + Document indexOptions = indexDefinition.getIndexOptions(); + IndexOptions ops = new IndexOptions(); - Document indexOptions = indexDefinition.getIndexOptions(); - IndexOptions ops = new IndexOptions(); - - if (indexOptions.containsKey("name")) { - ops = ops.name(indexOptions.get("name").toString()); - } - if (indexOptions.containsKey("unique")) { - ops = ops.unique((Boolean) indexOptions.get("unique")); - } - if (indexOptions.containsKey("sparse")) { - ops = ops.sparse((Boolean) indexOptions.get("sparse")); - } - if (indexOptions.containsKey("background")) { - ops = ops.background((Boolean) indexOptions.get("background")); - } - if (indexOptions.containsKey("expireAfterSeconds")) { - ops = ops.expireAfter((Long) indexOptions.get("expireAfterSeconds"), TimeUnit.SECONDS); - } - if (indexOptions.containsKey("min")) { - ops = ops.min(((Number) indexOptions.get("min")).doubleValue()); - } - if (indexOptions.containsKey("max")) { - ops = ops.max(((Number) indexOptions.get("max")).doubleValue()); - } - if (indexOptions.containsKey("bits")) { - ops = ops.bits((Integer) indexOptions.get("bits")); - } - if (indexOptions.containsKey("default_language")) { - ops = ops.defaultLanguage(indexOptions.get("default_language").toString()); - } - if (indexOptions.containsKey("language_override")) { - ops = ops.languageOverride(indexOptions.get("language_override").toString()); - } - if (indexOptions.containsKey("weights")) { - ops = ops.weights((org.bson.Document) indexOptions.get("weights")); - } + if (indexOptions.containsKey("name")) { + ops = ops.name(indexOptions.get("name").toString()); + } + if (indexOptions.containsKey("unique")) { + ops = ops.unique((Boolean) indexOptions.get("unique")); + } + if (indexOptions.containsKey("sparse")) { + ops = ops.sparse((Boolean) indexOptions.get("sparse")); + } + if (indexOptions.containsKey("background")) { + ops = ops.background((Boolean) indexOptions.get("background")); + } + if (indexOptions.containsKey("expireAfterSeconds")) { + ops = ops.expireAfter((Long) indexOptions.get("expireAfterSeconds"), TimeUnit.SECONDS); + } + if (indexOptions.containsKey("min")) { + ops = ops.min(((Number) indexOptions.get("min")).doubleValue()); + } + if (indexOptions.containsKey("max")) { + ops = ops.max(((Number) indexOptions.get("max")).doubleValue()); + } + if (indexOptions.containsKey("bits")) { + ops = ops.bits((Integer) indexOptions.get("bits")); + } + if (indexOptions.containsKey("default_language")) { + ops = ops.defaultLanguage(indexOptions.get("default_language").toString()); + } + if (indexOptions.containsKey("language_override")) { + ops = ops.languageOverride(indexOptions.get("language_override").toString()); + } + if (indexOptions.containsKey("weights")) { + ops = ops.weights((org.bson.Document) indexOptions.get("weights")); + } - for (String key : indexOptions.keySet()) { - if (ObjectUtils.nullSafeEquals("2dsphere", indexOptions.get(key))) { - ops = ops.sphereVersion(2); - } + for (String key : indexOptions.keySet()) { + if (ObjectUtils.nullSafeEquals("2dsphere", indexOptions.get(key))) { + ops = ops.sphereVersion(2); } + } - if (indexOptions.containsKey("partialFilterExpression")) { - ops = ops.partialFilterExpression((org.bson.Document) indexOptions.get("partialFilterExpression")); - } + if (indexOptions.containsKey("partialFilterExpression")) { + ops = ops.partialFilterExpression((org.bson.Document) indexOptions.get("partialFilterExpression")); + } - if (indexOptions.containsKey("collation")) { - ops = ops.collation(fromDocument(indexOptions.get("collation", Document.class))); - } + if (indexOptions.containsKey("collation")) { + ops = ops.collation(fromDocument(indexOptions.get("collation", Document.class))); + } - if (indexOptions.containsKey("wildcardProjection")) { - ops.wildcardProjection(indexOptions.get("wildcardProjection", Document.class)); - } + if (indexOptions.containsKey("wildcardProjection")) { + ops.wildcardProjection(indexOptions.get("wildcardProjection", Document.class)); + } - if (indexOptions.containsKey("hidden")) { - ops = ops.hidden((Boolean) indexOptions.get("hidden")); - } + if (indexOptions.containsKey("hidden")) { + ops = ops.hidden((Boolean) indexOptions.get("hidden")); + } - return ops; - }; + return ops; } public static @Nullable Collation fromDocument(@Nullable Document source) { @@ -133,8 +113,4 @@ private static Converter getIndexDefinitionIndexO return org.springframework.data.mongodb.core.query.Collation.from(source).toMongoCollation(); } - private static Converter 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, 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);