🗃️ Fix #7: Add indexes (#12)

Co-authored-by: alexis.drai@etu.uca.fr <alexis.drai@etu.uca.fr>
Reviewed-on: #12
main
Alexis Drai 1 year ago
parent c817ccd5a7
commit a5c36ea01b

@ -2,15 +2,27 @@
- [About](#about)
- [🗂DCM](#dcm)
- [`Trainer`](#trainer)
- [`Pokemong`](#pokemong)
- [`Move`](#move)
- [`Type`](#type)
- [🧬UML Class diagram](#uml-class-diagram)
- [🗺NoSQL Schema Versioning Strategy](#nosql-schema-versioning-strategy)
- [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)
@ -46,15 +58,21 @@ Let's cover the entities and relationships in this Data Concept Model:
These are the individuals who capture and train `pokemongs`. They can engage in battles with other `trainers.`
* a `trainer` has fought between 0 and many `trainers`
* we will use *referencing*, since this is a reflexive relationship
* a `trainer` owns between 0 and many `pokemongs`
* we will use *referencing with denormalizing*, since `pokemongs` have lifecycles of their own
#### `Pokemong`
These are the creatures that `trainers` capture and train. They can be trained or wild.
* a `pokemong` is owned by 0 or 1 `trainer`
* we will use *referencing*, since `trainers` have lifecycles of their own, but no denormalizing, since no queries
need that
* a `pokemong` has 1 or 2 `types`
* a `pokemong` knows between 0 and 4 `moves`,
* we will use *embedding*, since `types` don't have lifecycles of their own
* a `pokemong` knows between 0 and 4 `moves`
* we will use *referencing with denormalizing*, since `moves` have lifecycles of their own
#### `Move`
@ -62,21 +80,25 @@ These are the abilities or actions that a `pokemong` can perform. This covers th
different `moves` can have different effects and powers depending on the type of the `pokemong` and the `move`.
* a `move` can be known by between 0 and zillions of `pokemongs`
* we will let `pokemongs` refer to `moves`, and not the other way around
* a `move` has 1 and only 1 `type`
* we will use *embedding*, since `types` don't have lifecycles of their own
#### `Type`
These define the elements or categories that a `pokemong` or a `move` can belong to.
* a `type` can define between 0 and zillions of `pokemongs`
* see [`Pokemong`](#pokemong)
* a `type` can define between 0 and zillions of `moves`
* see [`Move`](#move)
<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
@ -195,6 +217,39 @@ allowing the schema migration to happen gradually over time.
However, note that this strategy increases write operations to the database, which could affect application performance.
### 📇Indexes
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
Each entity (`Pokemong`, `Trainer`, `Move`) in the application has a corresponding service class. These service
@ -203,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`.
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
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`.
```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
@ -337,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
@ -372,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.

@ -1,427 +1,447 @@
{
"info": {
"_postman_id": "11aa7a76-83a4-4b92-940f-528e29b66df8",
"name": "PoKeMoNg",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "25802734"
},
"item": [
{
"name": "move",
"item": [
{
"name": "Create 1 move",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"name\": \"Bubble beam\",\r\n \"category\": \"PHYSICAL\",\r\n \"power\": 10,\r\n \"accuracy\": 85,\r\n \"type\": \r\n {\r\n \"name\": \"WATER\",\r\n \"weakAgainst\": [\"GRASS\"],\r\n \"effectiveAgainst\": [\"FIRE\", \"GROUND\"]\r\n },\r\n \"schemaVersion\": 2\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/move",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move"
]
}
},
"response": []
},
{
"name": "Get all moves",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/move",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move"
]
}
},
"response": []
},
{
"name": "Get 1 move",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move",
"60a64f7eae945a6e60b0e917"
]
}
},
"response": []
},
{
"name": "Update 1 move",
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"schemaVersion\": 2,\r\n \"name\": \"Ember UPDATED\",\r\n \"category\": \"SPECIAL\",\r\n \"power\": 40,\r\n \"accuracy\": 100,\r\n \"type\": {\r\n \"name\": \"FIRE\",\r\n \"weakAgainst\": [\r\n \"WATER\",\r\n \"GROUND\"\r\n ],\r\n \"effectiveAgainst\": [\r\n \"GRASS\"\r\n ]\r\n }\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move",
"60a64f7eae945a6e60b0e917"
]
}
},
"response": []
},
{
"name": "Delete 1 move",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e913",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move",
"60a64f7eae945a6e60b0e913"
]
}
},
"response": []
}
]
},
{
"name": "pokemong",
"item": [
{
"name": "Create 1 pkmn",
"protocolProfileBehavior": {
"disabledSystemHeaders": {
"content-type": true
}
},
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"nickname\": \"Blappity-bloop\",\r\n \"dob\": \"2023-05-07\",\r\n \"level\": 1,\r\n \"pokedexId\": 172,\r\n \"evoStage\": 1,\r\n \"evoTrack\": [\"PICHU\", \"PIKACHU\", \"RAICHU\"],\r\n \"types\": [\r\n {\r\n \"name\": \"ELECTRIC\",\r\n \"weakAgainst\": [\"GROUND\", \"ROCK\"],\r\n \"effectiveAgainst\": [\"WATER\", \"FLYING\"]\r\n }\r\n ],\r\n \"moveSet\": \r\n [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e917\",\r\n \"name\": \"Ember\"\r\n }\r\n ],\r\n \"schemaVersion\": 1\r\n}"
},
"url": {
"raw": "http://localhost:8080/pokemong",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong"
]
}
},
"response": []
},
{
"name": "Get all pkmn",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong"
]
}
},
"response": []
},
{
"name": "Get 1 pkmn",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"60a64f7eae945a6e60b0e911"
]
}
},
"response": []
},
{
"name": "Update 1 pkmn",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"schemaVersion\": 1,\n \"nickname\": \"Sparky UPDATED\",\n \"dob\": \"1994-02-18\",\n \"level\": 15,\n \"pokedexId\": 1,\n \"evoStage\": 1,\n \"evoTrack\": [\n \"BULBASAUR\",\n \"IVYSAUR\",\n \"VENUSAUR\"\n ],\n \"trainer\": \"60a64f7eae945a6e60b0e914\",\n \"types\": [\n {\n \"name\": \"GRASS\",\n \"weakAgainst\": [\n \"FIRE\"\n ],\n \"effectiveAgainst\": [\n \"WATER\",\n \"GROUND\"\n ]\n }\n ],\n \"moveSet\": [\n {\n \"id\": \"60a64f7eae945a6e60b0e912\",\n \"name\": \"Vine Whip\"\n }\n ]\n}"
},
"url": {
"raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"60a64f7eae945a6e60b0e911"
]
}
},
"response": []
},
{
"name": "Delete 1 pkmn",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e916",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"60a64f7eae945a6e60b0e916"
]
}
},
"response": []
},
{
"name": "Get all pkmn by nickname",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong/nickname/sparky",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"nickname",
"sparky"
]
}
},
"response": []
},
{
"name": "Gat all pkmn by date interval",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong/dob/1995-01-01/1999-01-01",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"dob",
"1995-01-01",
"1999-01-01"
]
}
},
"response": []
}
]
},
{
"name": "trainer",
"item": [
{
"name": "Create 1 trainer",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Bloop\",\r\n \"dob\": \"1997-02-18\",\r\n \"wins\": 1,\r\n \"losses\": 50,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e915\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/trainer",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer"
]
}
},
"response": []
},
{
"name": "Get all trainers",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/trainer",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer"
]
}
},
"response": []
},
{
"name": "Get 1 trainer",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer",
"60a64f7eae945a6e60b0e914"
]
}
},
"response": []
},
{
"name": "Update 1 trainer",
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Brock\",\r\n \"dob\": \"1994-02-18\",\r\n \"wins\": 60,\r\n \"losses\": 60,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e914\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e915",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer",
"60a64f7eae945a6e60b0e915"
]
}
},
"response": []
},
{
"name": "Delete 1 trainer",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer",
"60a64f7eae945a6e60b0e914"
]
}
},
"response": []
}
]
}
]
"info": {
"_postman_id": "11aa7a76-83a4-4b92-940f-528e29b66df8",
"name": "PoKeMoNg",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "25802734"
},
"item": [
{
"name": "move",
"item": [
{
"name": "Create 1 move",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"name\": \"Bubble beam\",\r\n \"category\": \"PHYSICAL\",\r\n \"power\": 10,\r\n \"accuracy\": 85,\r\n \"type\": \r\n {\r\n \"name\": \"WATER\",\r\n \"weakAgainst\": [\"GRASS\"],\r\n \"effectiveAgainst\": [\"FIRE\", \"GROUND\"]\r\n },\r\n \"schemaVersion\": 2\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/move",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move"
]
}
},
"response": []
},
{
"name": "Get all moves",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/move",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move"
]
}
},
"response": []
},
{
"name": "Get 1 move",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move",
"60a64f7eae945a6e60b0e917"
]
}
},
"response": []
},
{
"name": "Update 1 move",
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"schemaVersion\": 2,\r\n \"name\": \"Ember UPDATED\",\r\n \"category\": \"SPECIAL\",\r\n \"power\": 40,\r\n \"accuracy\": 100,\r\n \"type\": {\r\n \"name\": \"FIRE\",\r\n \"weakAgainst\": [\r\n \"WATER\",\r\n \"GROUND\"\r\n ],\r\n \"effectiveAgainst\": [\r\n \"GRASS\"\r\n ]\r\n }\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move",
"60a64f7eae945a6e60b0e917"
]
}
},
"response": []
},
{
"name": "Delete 1 move",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e913",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"move",
"60a64f7eae945a6e60b0e913"
]
}
},
"response": []
}
]
},
{
"name": "pokemong",
"item": [
{
"name": "Create 1 pkmn",
"protocolProfileBehavior": {
"disabledSystemHeaders": {
"content-type": true
}
},
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"nickname\": \"Blappity-bloop\",\r\n \"dob\": \"2023-05-07\",\r\n \"level\": 1,\r\n \"pokedexId\": 172,\r\n \"evoStage\": 1,\r\n \"evoTrack\": [\"PICHU\", \"PIKACHU\", \"RAICHU\"],\r\n \"types\": [\r\n {\r\n \"name\": \"ELECTRIC\",\r\n \"weakAgainst\": [\"GROUND\", \"ROCK\"],\r\n \"effectiveAgainst\": [\"WATER\", \"FLYING\"]\r\n }\r\n ],\r\n \"moveSet\": \r\n [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e917\",\r\n \"name\": \"Ember\"\r\n }\r\n ],\r\n \"schemaVersion\": 1\r\n}"
},
"url": {
"raw": "http://localhost:8080/pokemong",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong"
]
}
},
"response": []
},
{
"name": "Get all pkmn",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong"
]
}
},
"response": []
},
{
"name": "Get 1 pkmn",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"60a64f7eae945a6e60b0e911"
]
}
},
"response": []
},
{
"name": "Update 1 pkmn",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"schemaVersion\": 1,\n \"nickname\": \"Sparky UPDATED\",\n \"dob\": \"1994-02-18\",\n \"level\": 15,\n \"pokedexId\": 1,\n \"evoStage\": 1,\n \"evoTrack\": [\n \"BULBASAUR\",\n \"IVYSAUR\",\n \"VENUSAUR\"\n ],\n \"trainer\": \"60a64f7eae945a6e60b0e914\",\n \"types\": [\n {\n \"name\": \"GRASS\",\n \"weakAgainst\": [\n \"FIRE\"\n ],\n \"effectiveAgainst\": [\n \"WATER\",\n \"GROUND\"\n ]\n }\n ],\n \"moveSet\": [\n {\n \"id\": \"60a64f7eae945a6e60b0e912\",\n \"name\": \"Vine Whip\"\n }\n ]\n}"
},
"url": {
"raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"60a64f7eae945a6e60b0e911"
]
}
},
"response": []
},
{
"name": "Delete 1 pkmn",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e916",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"60a64f7eae945a6e60b0e916"
]
}
},
"response": []
},
{
"name": "Get many pkmn by nickname",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong/nickname/sparky",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"nickname",
"sparky"
]
}
},
"response": []
},
{
"name": "Get many pkmn by date interval",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong/dob/1995-01-01/1999-01-01",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"dob",
"1995-01-01",
"1999-01-01"
]
}
},
"response": []
},
{
"name": "Get mapping of all pkmn count by evo stage",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/pokemong/count-by-evo-stage",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"pokemong",
"count-by-evo-stage"
]
}
},
"response": []
}
]
},
{
"name": "trainer",
"item": [
{
"name": "Create 1 trainer",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Bloop\",\r\n \"dob\": \"1997-02-18\",\r\n \"wins\": 1,\r\n \"losses\": 50,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e915\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/trainer",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer"
]
}
},
"response": []
},
{
"name": "Get all trainers",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/trainer",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer"
]
}
},
"response": []
},
{
"name": "Get 1 trainer",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer",
"60a64f7eae945a6e60b0e914"
]
}
},
"response": []
},
{
"name": "Update 1 trainer",
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Brock\",\r\n \"dob\": \"1994-02-18\",\r\n \"wins\": 60,\r\n \"losses\": 60,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e914\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e915",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer",
"60a64f7eae945a6e60b0e915"
]
}
},
"response": []
},
{
"name": "Delete 1 trainer",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"trainer",
"60a64f7eae945a6e60b0e914"
]
}
},
"response": []
}
]
}
]
}

@ -1,87 +0,0 @@
# About the database schema
## Collections
### trainers collection
* _id: ObjectId
* name: string
* (_indexed_: would often be queried in a dashboard situation)
* dob: date
* wins: int
* losses: int
* pastOpponents: array of ObjectId (references to other trainers)
* (_indexed_: reflexivity would make deep queries quite slow, so it seems worthwhile)
* pokemongs: array of ObjectId (references to owned pokemongs) + denormalizing on "nickname" and "species"
* (_indexed_: to improve speed when querying non-denormalized fields)
### pokemongs collection
* _id: ObjectId
* nickname: string?
* dob: date
* level: int
* pokedexId: int
* evoStage: int
* (_indexed_: "species" is calculated as evoTrack[evoStage], and would often be queried)
* evoTrack: array of strings (therefore "species" is evoTrack[evoStage], and "evoBase" is evoTrack[0])
* (_indexed_: "species" is calculated as evoTrack[evoStage], and would be queried often)
* trainer: ObjectId? (reference to a trainer) (but can be "wild" instead, if ref is null)
* (_indexed_: could be queried often in a dashboard situation)
* types: embedded type, or array of embedded types
* (_indexed_: would often be queried in a dashboard situation)
* moveSet: array of ObjectId (references to known moves) + denormalizing on "name"
### moves collection
* _id: ObjectId
* name: string
* (_indexed_: would often be queried in a dashboard situation)
* category: string (can be "physical", "special", or "status")
* power: int
* (_indexed_: would often be used in sorts, in a dashboard situation)
* accuracy: int
* type: embedded type
* (_indexed_: would often be queried in a dashboard situation)
### types collection
* _id: ObjectId
* name: string
* (_indexed_: would often be queried in a dashboard situation)
* weakAgainst: array of strings (denormalized type names)
* effectiveAgainst: array of strings (denormalized type names)
## Relationships
- Trainer
- [x] trainers.pastOpponents: one-to-many and reflexive
* => referencing
- [x] trainers.pokemongs: one-to-many
* => referencing + denormalizing on "nickname" and "species"
- Pokemong
- [x] pokemongs.trainer: many-to-one
* => referencing
- [x] pokemongs.types: one-to-few [1;2]
* => embedding
- [x] pokemongs.moveSet: one-to-few [0;4] but will also need to be queried independently
* => referencing + denormalizing on "name"
- Move
- [x] moves.type: one-to-one [1;1]
* => embedding
- Type
- [x] types.weakAgainst & types.effectiveAgainst: one-to-few, but reflexive
* => denormalizing on "name"
## Cascades
- Pokemong
- [x] delete ~> trainer.pokemongs
- [x] update ~> trainer.pokemongs (denormalizing on "nickname" and "species")
- [x] create ~> trainer.pokemongs
- Trainer
- [x] delete ~> pokemong.trainer
- [x] create ~> pokemong.trainer
- Move
- [x] delete ~> pokemong.moveSet
- [x] update ~> pokemong.moveSet (denormalizing on "name")

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