diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/MongoService.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/MongoService.java index 03187b0e2..823fe1a36 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/MongoService.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/MongoService.java @@ -1,6 +1,7 @@ package tech.ebp.oqm.core.api.service.mongo; import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.MongoCommandException; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; import com.mongodb.TransactionOptions; @@ -8,6 +9,7 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoIterable; import com.mongodb.client.model.IndexOptions; import jakarta.inject.Inject; import jakarta.validation.Validation; @@ -16,13 +18,19 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.bson.BsonDocument; +import org.bson.Document; import org.bson.conversions.Bson; import org.eclipse.microprofile.config.inject.ConfigProperty; import tech.ebp.oqm.core.api.model.collectionStats.CollectionStats; import tech.ebp.oqm.core.api.model.object.MainObject; import tech.ebp.oqm.core.api.model.rest.search.SearchObject; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** * This is the main mongo class. It specifies top level, commonly shared utilities. @@ -32,92 +40,161 @@ */ @Slf4j public abstract class MongoService, V extends CollectionStats> { - - //TODO:: move to constructor/inject? Remove? - protected static final Validator VALIDATOR; - - static { - try (ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory()) { - VALIDATOR = validatorFactory.getValidator(); - } - } - - /** - * Gets the default transaction options to use for client sessions. - * @return The default transaction options. - */ - public static TransactionOptions getDefaultTransactionOptions() { - return TransactionOptions.builder() - .readPreference(ReadPreference.primary()) - .readConcern(ReadConcern.LOCAL) - .writeConcern(WriteConcern.MAJORITY) - .build(); - } - - public ClientSession getNewClientSession(boolean startTransaction) { - ClientSession clientSession = this.getMongoClient().startSession(); - - if(startTransaction){ - clientSession.startTransaction(); - } - - return clientSession; - } - - public ClientSession getNewClientSession() { - return this.getNewClientSession(false); - } - - /** - * The default collection name to use when getting the collection. - * @param clazz The class to get the collection of - * @return The collection name to use when getting the collection. - */ - public static String getCollectionNameFromClass(Class clazz) { - return clazz.getSimpleName(); - } - - /** - * The class this collection is in charge of. Used for logging, and other fun. - */ - @Getter - protected final Class clazz; - - /** - * The MongoDb client. - */ - @Inject - @Getter(AccessLevel.PROTECTED) - MongoClient mongoClient; - - /** - * Mapper to help deal with json updates. - */ - @Inject - @Getter(AccessLevel.PROTECTED) - ObjectMapper objectMapper; - - /** - * The name of the database to access - */ - @Getter - @ConfigProperty(name = "quarkus.mongodb.database") - String databasePrefix; - - public MongoService(Class clazz) { - this.clazz = clazz; - } - - public abstract int getCurrentSchemaVersion(); - - public abstract List getDbIndexes(); - - public abstract void initDb(); - - protected static void setupIndexes(MongoCollection collection, List indexes) { - IndexOptions options = new IndexOptions().background(true); - for (Bson index : indexes) { - collection.createIndex(index, options); - } - } + + // TODO:: move to constructor/inject? Remove? + protected static final Validator VALIDATOR; + + static { + try (ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory()) { + VALIDATOR = validatorFactory.getValidator(); + } + } + + /** + * Gets the default transaction options to use for client sessions. + * + * @return The default transaction options. + */ + public static TransactionOptions getDefaultTransactionOptions() { + return TransactionOptions.builder().readPreference(ReadPreference.primary()).readConcern(ReadConcern.LOCAL).writeConcern(WriteConcern.MAJORITY).build(); + } + + public ClientSession getNewClientSession(boolean startTransaction) { + ClientSession clientSession = this.getMongoClient().startSession(); + + if (startTransaction) { + clientSession.startTransaction(); + } + + return clientSession; + } + + public ClientSession getNewClientSession() { + return this.getNewClientSession(false); + } + + /** + * The default collection name to use when getting the collection. + * + * @param clazz The class to get the collection of + * @return The collection name to use when getting the collection. + */ + public static String getCollectionNameFromClass(Class clazz) { + return clazz.getSimpleName(); + } + + /** + * The class this collection is in charge of. Used for logging, and other fun. + */ + @Getter + protected final Class clazz; + + /** + * The MongoDb client. + */ + @Inject + @Getter(AccessLevel.PROTECTED) + MongoClient mongoClient; + + /** + * Mapper to help deal with json updates. + */ + @Inject + @Getter(AccessLevel.PROTECTED) + ObjectMapper objectMapper; + + /** + * The name of the database to access + */ + @Getter + @ConfigProperty(name = "quarkus.mongodb.database") + String databasePrefix; + + public MongoService(Class clazz) { + this.clazz = clazz; + } + + public abstract int getCurrentSchemaVersion(); + + public abstract List getDbIndexes(); + + public abstract void initDb(); + + /** + * Sets up indexes for the given MongoDB collection. + * All expected indexes are (re)created on each call, and if an index already exists, it is dropped + * and recreated to ensure its options are always up to date. + * Indexes no longer present in the expected list are dropped. + * The default {@code _id_} index is never dropped. + * + * @param collection the MongoDB collection to manage indexes on + * @param indexes the list of indexes that should exist on the collection + */ + protected static void setupIndexes(MongoCollection collection, List indexes) { + log.info("setting up indexes for collection {}", collection.getNamespace()); + IndexOptions options = new IndexOptions().background(true); + Map existingIndexes = getExistingIndexes(collection); + Set expectedKeys = new HashSet<>(); + + for (Bson index : indexes) { + BsonDocument expectedKey = index.toBsonDocument(BsonDocument.class, collection.getCodecRegistry()); + expectedKeys.add(expectedKey); + log.info("ensuring index with key {}", expectedKey); + if (existingIndexes.containsKey(expectedKey)) { + try { + collection.createIndex(index, options); + } catch (MongoCommandException e) { + log.warn("failed to create index with key {}, dropping and recreating index", expectedKey, e); + collection.dropIndex(existingIndexes.get(expectedKey)); + collection.createIndex(index, options); + } + } else { + collection.createIndex(index, options); + } + } + + for (Map.Entry existing : existingIndexes.entrySet()) { + if (!expectedKeys.contains(existing.getKey())) { + log.info("dropping index with key {}", existing.getKey()); + collection.dropIndex(existing.getValue()); + } + } + } + + /** + * Returns a map of existing indexes on the given collection ignoring the default _id_ index. + * Input Index: + *
+     * {@code
+     * Indexes.ascending("name")
+     * }
+     * 
+ * + * Would produce: + *
+     * {@code
+     *   Key: {"name": 1}
+     *   Value: "name_1"
+     * }
+     * 
+ * + * @param collection the MongoDB collection to read indexes from + * @return map of index key document to index name + */ + private static Map getExistingIndexes(MongoCollection collection) { + MongoIterable existingDocument = collection.listIndexes(); + Map existingIndexes = new HashMap<>(); + for (Document doc : existingDocument) { + Document keyDoc = doc.get("key", Document.class); + if (keyDoc == null) { + continue; + } + BsonDocument key = keyDoc.toBsonDocument(BsonDocument.class, collection.getCodecRegistry()); + String name = doc.getString("name"); + if (name != null && !name.equals("_id_")) { + existingIndexes.put(key, name); + } + } + return existingIndexes; + } }