From ee864613205b0228247ebfb83e24bd9ba4864b86 Mon Sep 17 00:00:00 2001 From: "alexis.drai@etu.uca.fr" Date: Tue, 20 Jun 2023 13:33:00 +0200 Subject: [PATCH 1/4] :construction: WIP :memo: Update Readme --- README.md | 21 ++++++++++++++++++++- docs/DB.md | 5 ++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac76ab5..50695ad 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,15 @@ - [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) - [🐕‍🦺Services](#services) - [🌺Special requests](#special-requests) - [`Pokemong` by nickname](#pokemong-by-nickname) @@ -46,15 +51,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,14 +73,18 @@ 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) Data Concept Model @@ -195,6 +210,10 @@ 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 + +// TODO pick it up here + ### 🐕‍🦺Services Each entity (`Pokemong`, `Trainer`, `Move`) in the application has a corresponding service class. These service diff --git a/docs/DB.md b/docs/DB.md index 6ced79a..df1ea00 100644 --- a/docs/DB.md +++ b/docs/DB.md @@ -19,11 +19,14 @@ * _id: ObjectId * nickname: string? + * (_indexed_: this field already has a dedicated endpoint for a nickname search filter) * dob: date + * (_indexed_: this field already has a dedicated endpoint for a date of birth interval search filter) * level: int * pokedexId: int * evoStage: int - * (_indexed_: "species" is calculated as evoTrack[evoStage], and would often be queried) + * (_indexed_: "species" is calculated as evoTrack[evoStage], and would often be queried. + Besides, this field already has a dedicated endpoint for mapping evoStages with number of pokemongs at that stage) * 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) -- 2.36.3 From 1326501f443ea4c4fd20e7e9cb372ae930d4ceee Mon Sep 17 00:00:00 2001 From: "alexis.drai@etu.uca.fr" Date: Tue, 20 Jun 2023 13:34:25 +0200 Subject: [PATCH 2/4] :construction: WIP :memo: Update Readme :fire: remove draft notes --- .gitignore | 1 + docs/DB.md | 90 ------------------------------------------------------ 2 files changed, 1 insertion(+), 90 deletions(-) delete mode 100644 docs/DB.md diff --git a/.gitignore b/.gitignore index afb7952..4828079 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,4 @@ gradle-app.setting docs/todos.md /src/test/resources/application.properties /data/sample-dataset/load_data.sh +/docs/DB.md diff --git a/docs/DB.md b/docs/DB.md deleted file mode 100644 index df1ea00..0000000 --- a/docs/DB.md +++ /dev/null @@ -1,90 +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? - * (_indexed_: this field already has a dedicated endpoint for a nickname search filter) -* dob: date - * (_indexed_: this field already has a dedicated endpoint for a date of birth interval search filter) -* level: int -* pokedexId: int -* evoStage: int - * (_indexed_: "species" is calculated as evoTrack[evoStage], and would often be queried. - Besides, this field already has a dedicated endpoint for mapping evoStages with number of pokemongs at that stage) -* 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") -- 2.36.3 From 34ee915ceb84514f9314181b772ca45679735790 Mon Sep 17 00:00:00 2001 From: "alexis.drai@etu.uca.fr" Date: Tue, 20 Jun 2023 19:23:51 +0200 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Update=20Postman=20?= =?UTF-8?q?collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/postman_collection.json | 870 ++++++++++++++++++----------------- 1 file changed, 445 insertions(+), 425 deletions(-) diff --git a/data/postman_collection.json b/data/postman_collection.json index 8ccb223..6376749 100644 --- a/data/postman_collection.json +++ b/data/postman_collection.json @@ -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": [] + } + ] + } + ] } \ No newline at end of file -- 2.36.3 From b117f57484c473e760f92778c97c37d673ed3894 Mon Sep 17 00:00:00 2001 From: "alexis.drai@etu.uca.fr" Date: Tue, 20 Jun 2023 21:39:09 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Fix=20#7:=20Add=20i?= =?UTF-8?q?ndexes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - README.md | 108 ++++++++++++------ src/main/java/fr/uca/iut/Startup.java | 46 ++++++++ .../iut/repositories/GenericRepository.java | 13 ++- .../uca/iut/repositories/MoveRepository.java | 10 +- .../iut/repositories/PokemongRepository.java | 27 ++--- .../iut/repositories/TrainerRepository.java | 10 +- 7 files changed, 164 insertions(+), 51 deletions(-) create mode 100644 src/main/java/fr/uca/iut/Startup.java diff --git a/.gitignore b/.gitignore index 4828079..afb7952 100644 --- a/.gitignore +++ b/.gitignore @@ -86,4 +86,3 @@ gradle-app.setting docs/todos.md /src/test/resources/application.properties /data/sample-dataset/load_data.sh -/docs/DB.md diff --git a/README.md b/README.md index 50695ad..7d95d34 100644 --- a/README.md +++ b/README.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 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 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 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=
🏫 If you are the corrector -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. diff --git a/src/main/java/fr/uca/iut/Startup.java b/src/main/java/fr/uca/iut/Startup.java new file mode 100644 index 0000000..87927f0 --- /dev/null +++ b/src/main/java/fr/uca/iut/Startup.java @@ -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 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 repository) { + System.out.println("indexes for " + repository.getClass()); + for (Document index : repository.getCollection().listIndexes()) { + System.out.println(index.toJson()); + } + } +} diff --git a/src/main/java/fr/uca/iut/repositories/GenericRepository.java b/src/main/java/fr/uca/iut/repositories/GenericRepository.java index 1327e45..63ec20a 100644 --- a/src/main/java/fr/uca/iut/repositories/GenericRepository.java +++ b/src/main/java/fr/uca/iut/repositories/GenericRepository.java @@ -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 { * * @return The MongoDB collection of entities of type T. */ - protected abstract MongoCollection getCollection(); + public abstract MongoCollection getCollection(); /** * Inserts an entity into the collection. @@ -132,4 +133,14 @@ public abstract class GenericRepository { 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(); } diff --git a/src/main/java/fr/uca/iut/repositories/MoveRepository.java b/src/main/java/fr/uca/iut/repositories/MoveRepository.java index a8726c3..98ce5db 100644 --- a/src/main/java/fr/uca/iut/repositories/MoveRepository.java +++ b/src/main/java/fr/uca/iut/repositories/MoveRepository.java @@ -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 { } @Override - protected MongoCollection getCollection() { + public MongoCollection 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")); + } } diff --git a/src/main/java/fr/uca/iut/repositories/PokemongRepository.java b/src/main/java/fr/uca/iut/repositories/PokemongRepository.java index 409b29f..86af336 100644 --- a/src/main/java/fr/uca/iut/repositories/PokemongRepository.java +++ b/src/main/java/fr/uca/iut/repositories/PokemongRepository.java @@ -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 { } @Override - protected MongoCollection getCollection() { + public MongoCollection 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 { )) ); - MongoCollection 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); } } diff --git a/src/main/java/fr/uca/iut/repositories/TrainerRepository.java b/src/main/java/fr/uca/iut/repositories/TrainerRepository.java index 30eb66e..c47a32f 100644 --- a/src/main/java/fr/uca/iut/repositories/TrainerRepository.java +++ b/src/main/java/fr/uca/iut/repositories/TrainerRepository.java @@ -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 { } @Override - protected MongoCollection getCollection() { + public MongoCollection 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")); + } } -- 2.36.3