From 4b0c0274e85963b0a6fbaa85c2b7cc3e6a580e62 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 13 Apr 2023 15:11:28 +0200 Subject: [PATCH] Enable index modification via IndexOperations. Introduce IndexOptions that can be used to alter an existing index via IndexOperations. See: #4348 --- .../UncategorizedMongoDbException.java | 3 +- .../mongodb/core/DefaultIndexOperations.java | 16 ++ .../core/DefaultReactiveIndexOperations.java | 18 ++ .../data/mongodb/core/index/Index.java | 23 +-- .../mongodb/core/index/IndexOperations.java | 8 + .../core/index/IndexOperationsAdapter.java | 5 + .../data/mongodb/core/index/IndexOptions.java | 160 ++++++++++++++++++ .../core/index/ReactiveIndexOperations.java | 9 + ...efaultIndexOperationsIntegrationTests.java | 10 ++ .../DefaultReactiveIndexOperationsTests.java | 19 ++- 10 files changed, 254 insertions(+), 17 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java index 95b051e72..c69c780b2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java @@ -16,12 +16,13 @@ package org.springframework.data.mongodb; import org.springframework.dao.UncategorizedDataAccessException; +import org.springframework.lang.Nullable; public class UncategorizedMongoDbException extends UncategorizedDataAccessException { private static final long serialVersionUID = -2336595514062364929L; - public UncategorizedMongoDbException(String msg, Throwable cause) { + public UncategorizedMongoDbException(String msg, @Nullable Throwable cause) { super(msg, cause); } } 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 d31d16211..2f7860676 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 @@ -22,6 +22,7 @@ import java.util.List; import org.bson.Document; 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; @@ -29,6 +30,7 @@ import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; import com.mongodb.MongoException; import com.mongodb.client.MongoCollection; @@ -155,6 +157,20 @@ public class DefaultIndexOperations implements IndexOperations { } + @Override + 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))); + + 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); + } + } + public void dropAllIndexes() { dropIndex("*"); } 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 551bbb84a..1613c1eb1 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 @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.Optional; import org.bson.Document; +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; @@ -29,6 +30,7 @@ import org.springframework.data.mongodb.core.index.ReactiveIndexOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; import com.mongodb.client.model.IndexOptions; @@ -104,6 +106,22 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { }).next(); } + @Override + 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))) + .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); + } + }); + }).then(); + } + @Nullable private MongoPersistentEntity lookupPersistentEntity(String collection) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java index d8693e804..19b1732c0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java @@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.core.index.IndexOptions.Unique; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -39,11 +40,9 @@ public class Index implements IndexDefinition { private final Map fieldSpec = new LinkedHashMap(); private @Nullable String name; - private boolean unique = false; private boolean sparse = false; private boolean background = false; - private boolean hidden = false; - private long expire = -1; + private final IndexOptions options = IndexOptions.none(); private Optional filter = Optional.empty(); private Optional collation = Optional.empty(); @@ -71,7 +70,8 @@ public class Index implements IndexDefinition { * "https://docs.mongodb.org/manual/core/index-unique/">https://docs.mongodb.org/manual/core/index-unique/ */ public Index unique() { - this.unique = true; + + this.options.setUnique(Unique.YES); return this; } @@ -108,7 +108,8 @@ public class Index implements IndexDefinition { * @since 4.1 */ public Index hidden() { - this.hidden = true; + + options.setHidden(true); return this; } @@ -148,7 +149,7 @@ public class Index implements IndexDefinition { public Index expire(long value, TimeUnit unit) { Assert.notNull(unit, "TimeUnit for expiration must not be null"); - this.expire = unit.toSeconds(value); + options.setExpire(Duration.ofSeconds(unit.toSeconds(value))); return this; } @@ -200,21 +201,13 @@ public class Index implements IndexDefinition { if (StringUtils.hasText(name)) { document.put("name", name); } - if (unique) { - document.put("unique", true); - } if (sparse) { document.put("sparse", true); } if (background) { document.put("background", true); } - if (hidden) { - document.put("hidden", true); - } - if (expire >= 0) { - document.put("expireAfterSeconds", expire); - } + document.putAll(options.toDocument()); filter.ifPresent(val -> document.put("partialFilterExpression", val.getFilterObject())); collation.ifPresent(val -> document.append("collation", val.toDocument())); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperations.java index dbc09e86d..3af895cf9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperations.java @@ -35,6 +35,14 @@ public interface IndexOperations { */ String ensureIndex(IndexDefinition indexDefinition); + /** + * Drops an index from this collection. + * + * @param name name of index to hide. + * @since 4.1 + */ + void alterIndex(String name, IndexOptions options); + /** * Drops an index from this collection. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsAdapter.java index 7260bc00f..eef8a3bfb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsAdapter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsAdapter.java @@ -50,6 +50,11 @@ public interface IndexOperationsAdapter extends IndexOperations { reactiveIndexOperations.dropIndex(name).block(); } + @Override + public void alterIndex(String name, IndexOptions options) { + reactiveIndexOperations.alterIndex(name, options); + } + @Override public void dropAllIndexes() { reactiveIndexOperations.dropAllIndexes().block(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java new file mode 100644 index 000000000..9c7482c0b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java @@ -0,0 +1,160 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.index; + +import java.time.Duration; + +import org.bson.Document; +import org.springframework.lang.Nullable; + +/** + * Changeable properties of an index. Can be used for index creation and modification. + * + * @author Christoph Strobl + * @since 4.1 + */ +public class IndexOptions { + + @Nullable + private Duration expire; + + @Nullable + private Boolean hidden; + + @Nullable + private Unique unique; + + public enum Unique { + + NO, + + /** + * When unique is true the index rejects duplicate entries. + */ + YES, + + /** + * An existing index is not checked for pre-existing, duplicate index entries but inserting new duplicate entries + * fails. + */ + PREPARE + } + + /** + * @return new empty instance of {@link IndexOptions}. + */ + public static IndexOptions none() { + return new IndexOptions(); + } + + /** + * @return new instance of {@link IndexOptions} having the {@link Unique#YES} flag set. + */ + public static IndexOptions unique() { + + IndexOptions options = new IndexOptions(); + options.unique = Unique.YES; + return options; + } + + /** + * @return new instance of {@link IndexOptions} having the hidden flag set. + */ + public static IndexOptions hidden() { + + IndexOptions options = new IndexOptions(); + options.hidden = true; + return options; + } + + /** + * @return new instance of {@link IndexOptions} with given expiration. + */ + public static IndexOptions expireAfter(Duration duration) { + + IndexOptions options = new IndexOptions(); + options.unique = Unique.YES; + return options; + } + + /** + * @return the expiration time. A {@link Duration#isNegative() negative value} represents no expiration, {@literal null} if not set. + */ + public Duration getExpire() { + return expire; + } + + /** + * @param expire must not be {@literal null}. + */ + public void setExpire(Duration expire) { + this.expire = expire; + } + + /** + * @return {@literal true} if hidden, {@literal null} if not set. + */ + @Nullable + public Boolean isHidden() { + return hidden; + } + + /** + * @param hidden + */ + public void setHidden(boolean hidden) { + this.hidden = hidden; + } + + /** + * @return the unique property value, {@literal null} if not set. + */ + @Nullable + public Unique getUnique() { + return unique; + } + + /** + * @param unique must not be {@literal null}. + */ + public void setUnique(Unique unique) { + this.unique = unique; + } + + /** + * @return the store native representation + */ + public Document toDocument() { + + Document document = new Document(); + if(unique != null) { + switch (unique) { + case NO -> document.put("unique", false); + case YES -> document.put("unique", true); + case PREPARE -> document.put("prepareUnique", true); + } + } + if(hidden != null) { + document.put("hidden", hidden); + } + + + if (expire != null && !expire.isNegative()) { + document.put("expireAfterSeconds", expire.getSeconds()); + } + return document; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/ReactiveIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/ReactiveIndexOperations.java index f23a5556e..e0a02ffe2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/ReactiveIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/ReactiveIndexOperations.java @@ -35,6 +35,15 @@ public interface ReactiveIndexOperations { */ Mono ensureIndex(IndexDefinition indexDefinition); + /** + * Alters the index with given {@literal name}. + * + * @param name name of index to hide. + * @param + * @since 4.1 + */ + Mono alterIndex(String name, IndexOptions options); + /** * Drops an index from this collection. * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java index 7498f6e55..69895cdb7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java @@ -197,6 +197,16 @@ public class DefaultIndexOperationsIntegrationTests { assertThat(info.isHidden()).isTrue(); } + @Test // GH-4348 + void alterIndexShouldAllowHiding() { + + collection.createIndex(new Document("a", 1), new IndexOptions().name("my-index")); + + indexOps.alterIndex("my-index", org.springframework.data.mongodb.core.index.IndexOptions.hidden()); + IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "my-index"); + assertThat(info.isHidden()).isTrue(); + } + private IndexInfo findAndReturnIndexInfo(org.bson.Document keys) { return findAndReturnIndexInfo(indexOps.getIndexInfo(), keys); } 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 00febfcba..e754fe63b 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 @@ -39,6 +39,7 @@ import org.springframework.data.mongodb.test.util.MongoTemplateExtension; import org.springframework.data.mongodb.test.util.ReactiveMongoTestTemplate; import org.springframework.data.mongodb.test.util.Template; +import com.mongodb.client.model.IndexOptions; import com.mongodb.reactivestreams.client.MongoCollection; /** @@ -49,7 +50,7 @@ import com.mongodb.reactivestreams.client.MongoCollection; @ExtendWith(MongoTemplateExtension.class) public class DefaultReactiveIndexOperationsTests { - @Template(initialEntitySet = DefaultIndexOperationsIntegrationTestsSample.class) + @Template(initialEntitySet = DefaultIndexOperationsIntegrationTestsSample.class) // static ReactiveMongoTestTemplate template; String collectionName = template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class); @@ -192,6 +193,22 @@ public class DefaultReactiveIndexOperationsTests { .verifyComplete(); } + @Test // GH-4348 + void alterIndexShouldAllowHiding() { + + template.execute(collectionName, collection -> { + return collection.createIndex(new Document("a", 1), new IndexOptions().name("my-index")); + }).then().as(StepVerifier::create).verifyComplete(); + + indexOps.alterIndex("my-index", org.springframework.data.mongodb.core.index.IndexOptions.hidden()) + .as(StepVerifier::create).verifyComplete(); + indexOps.getIndexInfo().filter(this.indexByName("my-index")).as(StepVerifier::create) // + .consumeNextWith(indexInfo -> { + assertThat(indexInfo.isHidden()).isTrue(); + }) // + .verifyComplete(); + } + Predicate indexByName(String name) { return indexInfo -> indexInfo.getName().equals(name); }