Préambule

 

Ce premier TP consiste à utiliser les connaissances du 1er cours sur Spring Boot pour créer une API très simple, manipulant des données venant d'une BdD relationnelle de type H2. Cette base contient 2 tables qui devraient être normalement reliées par une relation one-to-many, grâce à une clef étrangère, mais qui dans ce TP seront reliées "à la main", dont avec des risques d'incohérence. Les TPs suivants consisteront à étoffer cette API et la base, afin d'utiliser un bon modèle relationnel.

 

1°/ Mise en place

La première étape est de créer le squelette du projet comme dans la démonstration n°2 du TD1, avec les dépendances nécessaires (Spring Web, JPA, H2).

Ensuite, il faut modifier le fichier application.properties afin d'utiliser une BdD de type H2 en mémoire, avec comme nom rpgbd. La base et les tables doivent être créées au lancement de l'application, et seront remplies grâce au fichier data.sql suivant :

INSERT INTO categories (name) VALUES
('helmet'), ('crown'), ('armor'), ('clothes'), ('weapon'),('lighter'), ('purse'),('potion'), ('spell'), ('food');
INSERT INTO rpgitems (name, id_category, price, effect) VALUES 
('conic helmet',1, 200.0, 'A+10'),
('great crown of apologia', 2, 200.0, 'A+20'),
('leather armor', 3, 100.0, 'A+10'),
('hauberk', 3, 500.0, 'A+40'),
('tuxedo', 4, 600.0, 'L+1'),
('unicorn cosplay',  4, 199.99, 'L+10'),
('dagger', 5 , 100.0, 'S+5'),
('long sword', 5 , 300.0, 'S+20'),
('torch', 6, 2.5, ''),
('leather purse', 7, 10.31, ''),
('protection potion', 8, 100.0, 'a+10'),
('fireball', 9, 1000.0, ''),
('ice cone', 9, 1000.0, ''),
('apple', 10, 1.1, 'l+1'),
('wine', 10, 9.75, 'l+2');

 

2°/ Cahier des charges

 

2.1°/ Les entités

Pour représenter les 2 tables utilisées dans ce TP, il faut créer deux classes d'entité nommés RpgItem et ItemCategory. La première classe doit être associée à la table nommée rpgitems et la seconde à la table categories.

Chacune de ces classes doit avoir un attribut représentant la clef primaire nommé id. Pour les autres attributs, leur nom et type peuvent être déduit de ce qu'il y a dans le fichier data.sql ci-dessus.

2.2°/ Les repository

Le TP nécessite de créer deux repository pour gérer chacune des tables, nommés ItemCategoryRepository et RpgItemRepository. Les deux héritent de JpaRepository mais seul le deuxième doit déclarer des méthodes supplémentaires pour :

  • trouver tous les items d'une certaine catégorie (donc un int),
  • trouver les items d'une certaine catégorie avec un prix inférieur à une valeur donnée (donc un double),
  • trouver les items dont le nom contient une chaîne donnée,
  • trouver les items qui n'ont pas d'effet (donc effect = '').

2.3°/ Les services

Le TP nécessite de créer deux services, dont les classes sont nommées ItemCategoryService et RpgItemService

La première classe définit deux méthodes :

  • List<ItemCategory> getAllCategories() : pour récupérer toutes les catégories existantes,
  • void createCategory(String name) : pour créer une nouvelle catégorie, dont le nom est donné en paramètre. Cette méthode fait quelque chose uniquement si le nom n'existe pas déjà dans la table.

La deuxième classe définit six méthodes :

  • List<RpgItem> getAllItems() : pour récupérer tous les items,
  • List<RpgItem> getItemsByCategory(String cat) : pour récupérer tous les items d'une catégorie, sachant que cette catégorie est paramétrée non pas comme un int mais comme une String. Pour que la méthode renvoie une liste non vide, il faut donc que la valeur de category se trouve dans la colonne name de la table categories (par ex, weapon, armor, ...) Cela vaut également pour les méthodes suivantes.
  • List<RpgItem> getItemsByCategoryUnderPrice(String cat, double price): pour récupérer tous les items d'une catégorie (également donnée sous forme de String) dont le prix est inférieur à une valeur donnée.
  • List<RpgItem>getItemsNameContaining(List<String> patterns) : pour récupérer tous les items dont le nom contient une des chaînes se trouvant dans la liste patterns.
  • List<RpgItem> getItemsNoEffect() : pour récupérer tous les items sans aucun effet.
  • void createItem(String name, String cat, double price, String effect) : pour créer un nouvel item.

2.4°/ Les contrôleurs

Le TP nécessite de créer deux services, dont les classes sont nommées ItemCategoryController et RpgItemController

La première classe doit définir 2 routes et leur méthode associée :

  • /rpg/categories, méthode GET : renvoie toutes les catégories,
  • /rpg/categories/{name}, méthode POST : pour créer une catégorie, sachant que le nom de la nouvelle catégorie est indiqué par le paramètre de route name.

La deuxième classe doit définir 7 routes et leur méthode associée :

  1. /rpg/items, méthode GET : renvoie tous les items,
  2. /rpg/items/{category}, méthode GET : renvoie tous les items de la catégorie indiquée par le paramètre category (en chaîne de caractères, cf. services)
  3. /rpg/items/{category}?maxprice=value, méthode GET : renvoie tous les items de la catégorie indiquée par le paramètre de route category, et dont le prix est inférieur à la valeur donnée par le paramètre de requête maxprice.
  4. /rpg/items?like=pattern1,pattern2,..., méthode GET : renvoie tous les items dont le nom contient un des patterns donné par le paramètre like de la requête.
  5. /rpg/items/noeffect, méthode GET : renvoie tous les items sans effet
  6. /rpg/items, méthode POST : pour créer un nouvel item, sachant que le corps de la requête est un objet JSON contenant au minimum le nom, la catégorie et le prix de l'item. L'effet est optionnel. La réponse doit contenir le nouvel objet créé.
  7. /rpg/items/{id}, méthode PUT : pour mettre à jour item dont l'id est donné par le paramètre de route id. Le corps de la requête est un objet JSON contenant un (au minimum) ou plusieurs champ parmi nom, catégorie, prix et effet. Seuls les champs existants sont utilisés pour mettre à jour l'item, s'il existe.

Remarques :

  • Le paramètre de route category, pour la 2 et 3, doit correspondre à un des noms de catégorie existante. Or, la table rpgitems contient l'id d'une catégorie et pas son nom. Avec un modèle relationnel bien fait, cet id devrait être une clef étrangère et l'on pourrait obtenir directement les items d'une certaine catégorie grâce à une requête SQL avec jointure. Ce n'est pas le cas dans ce TP. Il faut donc faire une sorte de jointure "à la main", donc en allant vérifier dans la table categories s'il en existe une avec un nom donnée, afin de renvoyer son id, ou bien null si elle n'existe pas. Pour ce faire, la solution la plus élégante consiste à créer une telle méthode de service dans ItemCategoryService. Ensuite, si RpgItemService contient un attribut de type ItemCategoryService (initialisé grâce au constructeur @AutoWired), on pourra appeler cette méthode. On obtient donc un service qui en utilise un autre, ce qui est en fait courant. Enfin, on peut écrire encore moins de code en créant une méthode dans ItemCategoryRepository qui renvoie une catégorie par son nom. 

 

  • Pour créer les routes 2 et 3 pour les items, il suffit d'une seule méthode mais avec un paramètre de requête optionnel (cf. doc @RequestParam). Cependant, pour que la route fonctionne, il faut que le paramètre de la méthode représentant le paramètre de requête soit de type objet. En effet, si le paramètre de requête n'est pas donné dans l'URL, le paramètre de la méthode pourra avoir comme valeur null, ce qui ne serait pas possible avec des types primaires comme int, double, ... Dans le cas présent, cela veut dire que la méthode de route doit avoir un paramètre de type Double (grand D) pour représenté la valeur de maxprice.
  • C'est le même principe pour créer les routes 1 et 4, excepté que la méthode doit prendre un String en paramètre.

 

  • Le TD2 a abordé le principe d'un mapping de requête POST ou PUT, et notamment comment récupérer le corps de la requête. D'après ces principes :
    • l'objet JSON envoyé lors de la création ou modification d'un item ne contient pas tous les champs correspondant à l'entité RpgItem, ou bien avec d'autres types. Spring ne peut donc pas "traduire" le JSON reçu directement en une instance de RpgItem. On est donc obligé de créer une nouvelle classe dans le package model (par ex, nommée RpgItemDTO, NB : DTO = Data Transaction Object) pour représenter l'objet JSON reçu, que Spring peut utiliser pour traduire le JSON en une instance de cette classe. Les valeurs des attributs de cette instance peuvent ensuite être utilisés pour faire appel au service qui va créer l'item en BdD.
    • Comme on est en Java, un objet ne peut pas avoir moins d'attributs que prévu, comme en JS. Cela pose problème pour la route 7. En effet, le client peut envoyer un objet JSON avec un nombre variable de champs pour mettre à jour un item. Pourtant, il faut bien que Spring puisse traduire ce JSON en un objet de la classe RpgItemDTO. Une solution consiste à créer autant de constructeur que de possibilités mais ce n'est pas pratique, voire impossible. La solution consiste à ne déclarer dans la classe ...DTO que des attributs de type objet. Par exemple, pour la classe RpgItemDTO, on déclare un attribut Long idCategory, au lieu du type primaire long, et Double price, au lieu de double. Dans ce cas, si le JSON reçu ne contient pas de champ price, Spring va créer un objet dont l'attribut price vaut null. On peut ainsi savoir ce qu'il faut mettre à jour ou pas.
  • Même quand l'objet reçu a la même structure qu'une des entités du modèle, c'est une bonne pratique que de créer une classe spéciale pour représenter l'objet reçu. Cela permet de découpler encore plus la partie réception de données (donc le contrôle) et celle qui gère les accès BdD (donc les services). 

 

3°/ Tester les routes

La façon la plus simple et la plus universelle consiste à utiliser Postman. Cela dit, pour les requête GET, un simple navigateur suffit.