Enable index modification via IndexOperations.

Introduce IndexOptions that can be used to alter an existing index via IndexOperations.

See: #4348
This commit is contained in:
Christoph Strobl
2023-04-13 15:11:28 +02:00
parent 83217f3413
commit 4b0c0274e8
10 changed files with 254 additions and 17 deletions

View File

@@ -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);
}
}

View File

@@ -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("*");
}

View File

@@ -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<Void> 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) {

View File

@@ -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<String, Direction> fieldSpec = new LinkedHashMap<String, Direction>();
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<IndexFilter> filter = Optional.empty();
private Optional<Collation> 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/</a>
*/
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()));

View File

@@ -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.
*

View File

@@ -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();

View File

@@ -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;
}
}

View File

@@ -35,6 +35,15 @@ public interface ReactiveIndexOperations {
*/
Mono<String> ensureIndex(IndexDefinition indexDefinition);
/**
* Alters the index with given {@literal name}.
*
* @param name name of index to hide.
* @param
* @since 4.1
*/
Mono<Void> alterIndex(String name, IndexOptions options);
/**
* Drops an index from this collection.
*

View File

@@ -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);
}

View File

@@ -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<IndexInfo> indexByName(String name) {
return indexInfo -> indexInfo.getName().equals(name);
}