🗃️ Fix #7: Add indexes

Alexis Drai 1 year ago
parent 34ee915ceb
commit 50802078a0

@ -11,11 +11,18 @@
- [Schema Versioning Pattern](#schema-versioning-pattern)
- [Incremental Document Migration](#incremental-document-migration)
- [📇Indexes](#indexes)
- [`moves` collection](#moves-collection)
- [`pokemongs` collection](#pokemongs-collection)
- [`trainers` collection](#trainers-collection)
- [🐕🦺Services](#services)
- [🌺Special requests](#special-requests)
- [`Pokemong` by nickname](#pokemong-by-nickname)
- [`Pokemong` in date interval](#pokemong-in-date-interval)
- [🦚Aggregation pipeline](#aggregation-pipeline)
- [👔Some business rules](#some-business-rules)
- [`Move` CRUD cascade](#move-crud-cascade)
- [`Pokemong` CRUD cascade](#pokemong-crud-cascade)
- [`Trainer` CRUD cascade](#trainer-crud-cascade)
- [Prep steps](#prep-steps)
- [Java version](#java-version)
- [🔐Database connection](#database-connection)
@ -212,7 +219,36 @@ However, note that this strategy increases write operations to the database, whi
### 📇Indexes
// TODO pick it up here
Various indexes were created for fields that would often be queried in a dashboard situation. If there is an additional
reason, it will be specified below.
Unless otherwise specified, please consider indexes to be full, and ascending.
#### `moves` collection
In the front-end app, these are queried both in the detail screen and in the list screen.
* `name`
* `power`: Descending, because users are more likely to sort them in that order.
* `type`
#### `pokemongs` collection
* `nickname`: This field already has a dedicated endpoint for a nickname search filter.
* `dob`: Descending, because users are more likely to sort them in that order.
* `evoStage`: "Species" is calculated as `evoTrack[evoStage]`, and would often be queried.
* `evoTrack`: See `evoStage`. Yes, it's an array, but it's a one-to-few relationship.
* `trainer`: Partial index, to avoid indexing wild pokemongs there.
* `types`: It's an array, but it's a one-to-few relationship.
#### `trainers` collection
It was tempting to index `pastOpponents` and `pokemongs` in the `trainers` collection, but this array
could grow indefinitely, and the index may grow so large that it wouldn't fit in a server's RAM anymore.
* `name`
* `wins`: Descending, because users are more likely to sort them in that order for rankings.
* `losses`: Descending, because users are more likely to sort them in that order for rankings.
### 🐕🦺Services
@ -356,6 +392,30 @@ As an example of a potential output:
]
```
### 👔Some business rules
#### `Move` CRUD cascade
* When you delete a `move`, it also gets deleted from any `pokemong`'s `moveSet`.
* Since `pokemongMove` is denormalized on the `name` field, that field also gets updated when a `move`'s `name` is
updated.
#### `Pokemong` CRUD cascade
* When a `pokemong` is created, the new `pokemong`'s information is also added to the `pokemongs` array of any
associated `trainer` documents.
* When a `pokemong` is deleted, the `pokemongs` array in the associated `trainer` documents also has that specific
`pokemong` removed.
* Since `trainerPokemong` is denormalized on the `nickname` and `species` fields, those fields also get updated when
a `pokemong`'s `nickname` is updated, or when a `pokemong` evolves.
#### `Trainer` CRUD cascade
* When a `trainer` is created, the new `trainer`'s information is also updated in the `trainer` field of any associated
`pokemong` documents. Since a `pokemong` can only belong to one `trainer` at a time, that may mean removing it from
one to give it to the other.
* When a `trainer` is deleted, the `trainer` field in the associated `pokemong` documents is also removed.
## Prep steps
### ♨Java version

@ -0,0 +1,46 @@
package fr.uca.iut;
import fr.uca.iut.entities.GenericEntity;
import fr.uca.iut.repositories.GenericRepository;
import fr.uca.iut.repositories.MoveRepository;
import fr.uca.iut.repositories.PokemongRepository;
import fr.uca.iut.repositories.TrainerRepository;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import org.bson.Document;
@ApplicationScoped
public class Startup {
@Inject
MoveRepository moveRepository;
@Inject
PokemongRepository pokemongRepository;
@Inject
TrainerRepository trainerRepository;
void onStart(@Observes StartupEvent ev) {
createIndex(moveRepository);
createIndex(pokemongRepository);
createIndex(trainerRepository);
}
private void createIndex(GenericRepository<? extends GenericEntity> repository) {
try {
repository.createIndexes();
printIndexes(repository);
} catch (Exception e) {
System.err.println("Error creating indexes for repository: " + repository.getClass());
e.printStackTrace();
}
}
private void printIndexes(GenericRepository<? extends GenericEntity> repository) {
System.out.println("indexes for " + repository.getClass());
for (Document index : repository.getCollection().listIndexes()) {
System.out.println(index.toJson());
}
}
}

@ -2,6 +2,7 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.ReplaceOneModel;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.WriteModel;
@ -56,7 +57,7 @@ public abstract class GenericRepository<T extends GenericEntity> {
*
* @return The MongoDB collection of entities of type T.
*/
protected abstract MongoCollection<T> getCollection();
public abstract MongoCollection<T> getCollection();
/**
* Inserts an entity into the collection.
@ -132,4 +133,14 @@ public abstract class GenericRepository<T extends GenericEntity> {
Document query = new Document("_id", new ObjectId(id));
return getCollection().countDocuments(query) > 0;
}
/**
* @return the MongoDB database
*/
@NotNull
public MongoDatabase getMongoDatabase() {
return mongoClient.getDatabase(DB_NAME);
}
public abstract void createIndexes();
}

@ -3,6 +3,7 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Indexes;
import fr.uca.iut.entities.Move;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
@ -24,8 +25,15 @@ public class MoveRepository extends GenericRepository<Move> {
}
@Override
protected MongoCollection<Move> getCollection() {
public MongoCollection<Move> getCollection() {
MongoDatabase db = mongoClient.getDatabase(DB_NAME);
return db.getCollection(Move.COLLECTION_NAME, Move.class);
}
@Override
public void createIndexes() {
getCollection().createIndex(Indexes.ascending("name"));
getCollection().createIndex(Indexes.descending("power"));
getCollection().createIndex(Indexes.ascending("type"));
}
}

@ -3,10 +3,7 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Accumulators;
import com.mongodb.client.model.Aggregates;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Projections;
import com.mongodb.client.model.*;
import fr.uca.iut.entities.Pokemong;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
@ -14,7 +11,6 @@ import jakarta.inject.Inject;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.jetbrains.annotations.NotNull;
import java.time.LocalDate;
import java.time.ZoneId;
@ -45,16 +41,11 @@ public class PokemongRepository extends GenericRepository<Pokemong> {
}
@Override
protected MongoCollection<Pokemong> getCollection() {
public MongoCollection<Pokemong> getCollection() {
MongoDatabase db = getMongoDatabase();
return db.getCollection(Pokemong.COLLECTION_NAME, Pokemong.class);
}
@NotNull
private MongoDatabase getMongoDatabase() {
return mongoClient.getDatabase(DB_NAME);
}
/**
* Fetches the list of Pokemong entities that have a nickname matching the provided nickname.
* The match is case-insensitive and ignores leading and trailing spaces.
@ -108,8 +99,18 @@ public class PokemongRepository extends GenericRepository<Pokemong> {
))
);
MongoCollection<Document> collection = getMongoDatabase().getCollection(getCollection().getNamespace().getCollectionName());
return collection.aggregate(pipeline, Document.class).into(new ArrayList<>());
return getCollection().aggregate(pipeline, Document.class).into(new ArrayList<>());
}
@Override
public void createIndexes() {
getCollection().createIndex(Indexes.ascending("nickname"));
getCollection().createIndex(Indexes.descending("dob"));
getCollection().createIndex(Indexes.ascending("evoStage"));
getCollection().createIndex(Indexes.ascending("evoTrack"));
getCollection().createIndex(Indexes.ascending("types"));
IndexOptions indexOptions = new IndexOptions().partialFilterExpression(Filters.exists("trainer", true));
getCollection().createIndex(Indexes.ascending("trainer"), indexOptions);
}
}

@ -3,6 +3,7 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Indexes;
import fr.uca.iut.entities.Trainer;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
@ -24,8 +25,15 @@ public class TrainerRepository extends GenericRepository<Trainer> {
}
@Override
protected MongoCollection<Trainer> getCollection() {
public MongoCollection<Trainer> getCollection() {
MongoDatabase db = mongoClient.getDatabase(DB_NAME);
return db.getCollection(Trainer.COLLECTION_NAME, Trainer.class);
}
@Override
public void createIndexes() {
getCollection().createIndex(Indexes.ascending("name"));
getCollection().createIndex(Indexes.descending("wins"));
getCollection().createIndex(Indexes.descending("losses"));
}
}

Loading…
Cancel
Save