🗃️ Fix #7: Add indexes

pull/12/head
Alexis Drai 1 year ago
parent 34ee915ceb
commit b117f57484

1
.gitignore vendored

@ -86,4 +86,3 @@ gradle-app.setting
docs/todos.md
/src/test/resources/application.properties
/data/sample-dataset/load_data.sh
/docs/DB.md

@ -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)
@ -88,10 +95,10 @@ These define the elements or categories that a `pokemong` or a `move` can belong
<img src="./docs/mcd.png" alt="Data Concept Model" title="Data Concept Model">
Looking at things from the point of view of entities, instead of relationships
### 🧬UML Class diagram
Omitting some details, our entities look like this:
```mermaid
classDiagram
@ -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 these arrays
could grow indefinitely, and the indexes may grow so large that they 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
@ -222,39 +258,19 @@ database through their associated repositories, performing CRUD operations.
All service classes inherit from a `GenericService` class, which provides the following methods:
* addOne(T entity): Adds a new entity to the database, after validating it.
* getOneById(String id): Retrieves a single entity from the database by its ID.
* getAll(): Retrieves all entities of a certain type from the database.
* deleteOneById(String id): Deletes an entity from the database by its ID.
* updateOne(T entity): Updates an existing entity in the database. This method is meant to be overridden in child
service
classes to provide the specific update logic for each type of entity.
* updateAll(List<T> entities): Updates all entities in a given list. Each entity is validated before updating.
* `addOne(T entity)`: Adds a new entity to the database, after validating it.
* `getOneById(String id)`: Retrieves a single entity from the database by its ID.
* `getAll()`: Retrieves all entities of a certain type from the database.
* `deleteOneById(String id)`: Deletes an entity from the database by its ID.
* `updateOne(T entity)`: Updates an existing entity in the database. This method is meant to be overridden in child
service classes to provide the specific update logic for each type of entity.
* `updateAll(List<T> entities)`: Updates all entities in a given list. Each entity is validated before updating.
These methods allow the application to perform all the basic CRUD operations on any type of entity. The specific logic
for each type of entity (like how to validate a Pokemong, how to update a Move, etc.) is provided in the child service
classes that inherit from `GenericService`.
for each type of entity (like how to validate a `pokemong`, how to update a `move`, etc.) is provided in the child
service classes that inherit from `GenericService`.
Many business rules were applied, so let's use just one for an example here: when a `trainer` gets updated, it can mean
consequences for any number of `pokemongs`, as this commented code from inside `TrainerService.UpdateOne()` explains
```java
// all old pokemongs who are not there anymore lose their trainer reference
pokemongService.batchUpdatePokemongTrainers(
oldPokemongs.stream()
.filter(tp->!newPokemongs.contains(tp))
.collect(Collectors.toSet()),
null);
// if this trainer gained a pokemong, that pokemong's ex-trainer if any needs to lose said pokemong
transferNewlyArrivedTrainerPokemongs(oldPokemongs,newPokemongs);
// all new pokemongs who were not there before gain this trainer's reference
pokemongService.batchUpdatePokemongTrainers(
newPokemongs.stream()
.filter(tp->!oldPokemongs.contains(tp))
.collect(Collectors.toSet()),
existingTrainer.getId()
);
```
Many business rules were applied, which can be browsed [here](#some-business-rules).
This diagram attempts to show the relationship between services in this API
@ -356,6 +372,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
@ -391,7 +431,7 @@ quarkus.mongodb.database=<database>
<details><summary>🏫 If you are the corrector</summary>
To be able to use this app, update `application.properties` with the provided database secrets.
To be able to use this app, please update `application.properties` with the provided database secrets.
If none were provided, that was a mistake. Sorry. Please request them to the owner of this repo.

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