Ajoutez une base de données intégrée et générez les tables
Transformez la classe Product en entité
Afin de pouvoir générer nos tables, nous avons besoin de transformer notre classe Product en entité gérée par JPA.
Nous allons commencer par importer toutes les dépendances nécessaires à l'utilisation du JPA. Spring Boot nous offre pour cela un starter prêt à l'emploi : spring-boot-starter-data-jpa. Commençons par l'ajouter dans la balise <dependencies>
de notre pom.xml, comme illustré ci-dessous :
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Afin de nous assurer que les dépendances ont bien été importées, nous pouvons cliquer sur l'icône de rafraîchissement du projet Maven fournie par IntelliJ, comme le montre la figure suivante :
Nous pouvons désormais ajouter les annotations @Entity
et @Id
à la classe Product. Modifiez donc la classe pour obtenir le code ci-après :
package com.ecommerce.micrommerce.web.model;
import com.fasterxml.jackson.annotation.JsonFilter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
//@JsonFilter("monFiltreDynamique")
@Entity
public class Product {
@Id
private int id;
private String nom;
private int prix;
//information que nous ne souhaitons pas exposer
private int prixAchat;
...
}
Explications :
Vous annotez la classe avec @Entity afin qu'elle soit scannée et prise en compte. Il n'y a ainsi pas besoin de passer par le traditionnel (et complexe) fichier persistence.xml.
Vous annotez l'attribut id avec @Id et @GeneratedValue afin qu'il soit identifié en tant que clé unique autogénérée.
Ajoutez une base de données
Dans un cas réel, notre microservice doit communiquer avec une base de données qui lui est dédiée, disponible sur un serveur distant. Pour nos tests, nous allons utiliser une base de données H2 qui est intégrable directement dans notre microservice. H2 est une base de données très légère (1 Mo). Elle va créer les tables et les données uniquement en mémoire vive. Une fois l'application fermée, ces données sont perdues. Cette approche est très utile quand vous développez un microservice, car elle permet de refaire les tests autant de fois que nécessaire, en partant d'une base de données propre. L'autre avantage est qu'elle est très simple à mettre en place, et complètement autoconfigurée par Spring Boot.
Modifiez le fichier pom.xml afin d'ajouter H2, comme illustré ci-après :
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Actualisez Maven afin d'importer la dépendance. Nous allons maintenant modifier application.properties afin de remplacer les configurations par défaut, et demander à Spring d'afficher les requêtes SQL et d'activer l'interface graphique H2, ce qui permettra de visualiser nos tables.
Complétez le fichier application.properties afin d''obtenir le fichier suivant :
server.port 9090
spring.jpa.show-sql=true
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=none
L'entité et la base de données sont désormais prêtes à l'emploi !
Il nous reste une dernière étape : configurer la table Product qui va être créée pour qu'elle contienne des données par défaut. Vous n'allez tout de même pas les rentrer à la main à chaque fois que vous redémarrez votre application ?
Créez tout simplement un fichier data.sql dans le dossier “resources” et écrivez les requêtes SQL nécessaires à l'insertion de vos données. Dans notre cas, nous allons utiliser les mêmes données que nous avions écrites en dur dans le DAO :
INSERT INTO product VALUES(1, 'Ordinateur portable' , 350, 120);
INSERT INTO product VALUES(2, 'Aspirateur Robot' , 500, 200);
INSERT INTO product VALUES(3, 'Table de Ping Pong' , 750, 400);
Nous avons aussi besoin de créer un fichier schema.sql pour définir les tables de notre base de données.
CREATE TABLE product
(
id INT PRIMARY KEY,
nom VARCHAR(255) NOT NULL,
prix INT NOT NULL,
prix_achat INT NOT NULL
);
C'est tout ! Ce fichier sera récupéré automatiquement, puis exécuté dans la base de données une fois que la table sera créée.
Redémarrez votre application et rendez-vous sur la console de H2 à l'adresse http://localhost:9090/h2-console/. Dans le champ JDBC URL, saisissez jdbc:h2:mem:testdb
afin de configurer la connexion vers la base de données testdb située en mémoire vive.
Cliquez ensuite sur Connect. La console vous propose alors une interface permettant de visualiser la table PRODUCT. Celle-ci a été créée grâce à l'annotation @Entity définie pour la classe Product. Cette table contient comme prévu les données de notre fichier data.sql, comme l'illustre la figure suivante :
Utilisez Spring Data JPA
Créez un Repository et utilisez les opérations CRUD autogénérées
Spring Data JPA est un autre framework de Spring, qui facilite grandement l'utilisation de JPA. Il va nous permettre de générer toutes sortes d'opérations vers la base de données, sans que nous ayons à écrire la moindre requête, ni même la moindre implémentation DAO. Il nous suffit d'hériter de l'interface JpaRepository.
Rendez-vous dans le fichier ProductDao et modifiez-le comme suit :
package com.ecommerce.micrommerce.web.dao;
import com.ecommerce.micrommerce.web.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductDao extends JpaRepository<Product, Integer> {
Product findById(int id);
}
Quand vous héritez de JpaRepository, il faut indiquer comme premier paramètre l'entité concernée, puis le type d'Id. Dans ce cas, Product et Integer. C'est tout !
Supprimez la classe ProductDaoImpl
qui n'est plus nécessaire
Dans le contrôleur (ProductController), commentez toutes les méthodes sauf listeProduits, afin que l'on puisse les modifier et les tester une à une :
package com.ecommerce.microcommerce.web.controller;
import com.ecommerce.microcommerce.dao.ProductDao;
import com.ecommerce.microcommerce.model.Product;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.List;
@RestController
public class ProductController {
@Autowired
private ProductDao productDao;
//Récupérer la liste des produits
@RequestMapping(value = "/Produits", method = RequestMethod.GET)
public MappingJacksonValue listeProduits() {
Iterable<Product> produits = productDao.findAll();
SimpleBeanPropertyFilter monFiltre = SimpleBeanPropertyFilter.serializeAllExcept("prixAchat");
FilterProvider listDeNosFiltres = new SimpleFilterProvider().addFilter("monFiltreDynamique", monFiltre);
MappingJacksonValue produitsFiltres = new MappingJacksonValue(produits);
produitsFiltres.setFilters(listDeNosFiltres);
return produitsFiltres;
}
//Récupérer un produit par son Id
/* @GetMapping(value = "/Produits/{id}")
public Product afficherUnProduit(@PathVariable int id) {
return productDao.findById(id);
}
//ajouter un produit
@PostMapping(value = "/Produits")
public ResponseEntity<Void> ajouterProduit(@RequestBody Product product) {
Product productAdded = productDao.save(product);
if (productAdded == null)
return ResponseEntity.noContent().build();
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(productAdded.getId())
.toUri();
return ResponseEntity.created(location).build();
}*/
}
Parmi les opérations qu'offre JpaRepository, nous trouvons findAll() qui permet de récupérer toutes les données de l'entité concernée. findAll() retourne un Iterable, qu'il nous faut spécifier comme type de retour de la méthode.
Pour vérifier le bon fonctionnement, lancez votre application puis rendez-vous sur http://localhost:9090/Produits. Vous pouvez admirer le résultat, sorti tout droit de la base de données. Vous pouvez le vérifier en changeant une valeur dans celle-ci, comme illustré ci-dessous :
Nous obtenons ainsi :
[
{
"id": 1,
"nom": "Ordinateur portable changé !!",
"prix": 350
},
{
"id": 2,
"nom": "Aspirateur Robot",
"prix": 500
},
{
"id": 3,
"nom": "Table de Ping Pong",
"prix": 750
}
]
Les autres principales opérations autogénérées que nous pouvons utiliser sont :
delete(int id) : supprime le produit correspondant à l'Id passé en argument.
count() : calcule le nombre de produits.
save(Product produit) : ajoute le produit passé en argument. Cette méthode peut également recevoir un Iterable<> de produits pour ajouter plusieurs produits.
Vous pouvez consulter la liste complète ici.
Utilisez les requêtes générées par le nom de la méthode
La magie de Spring Data va au-delà des requêtes CRUD. Le framework est capable de générer la requête SQL automatiquement en partant du nom de votre méthode !
Prenons un exemple : tout d'abord, ajoutons cette méthode à notre Repository :
@Repository
public interface ProductDao extends JpaRepository<Product, Integer>
{
Product findById(int id);
}
Puis retournons dans ProductController et décommentons afficherUnProduit :
//Récupérer un produit par son Id
@GetMapping(value = "/Produits/{id}")
public Product afficherUnProduit(@PathVariable int id)
{
return productDao.findById(id);
}
Une fois l'application démarrée, nous obtenons le bon résultat sans avoir écrit la moindre requête.
Mais comment est-ce possible ?
Les conventions ! Spring Data JPA propose un ensemble de conventions qui lui permettront de déduire la bonne requête à partir du nom de la méthode. Vous avez ici l'ensemble des mots clés acceptés.
Prenons comme exemple la récupération des produits dont le prix est supérieur à 400. Commençons par ajouter une méthode findByPrixGreaterThan au Repository, à l'intérieur de la définition de l'interface ProductDAO :
@Repository
public interface ProductDao extends JpaRepository<Product, Integer>
{
Product findById(int id);
List<Product> findByPrixGreaterThan(int prixLimit);
}
Toute la logique est fournie par le nom de la méthode !
findBy : indique que l'opération à exécuter est un SELECT.
Prix : fournit le nom de la propriété sur laquelle le SELECT s'applique.
GreaterThan : définit une condition "plus grand que".
La valeur à appliquer à la condition est, quant à elle, définie par le paramètre prixLimit.
Cette méthode génère une requête équivalant au pseudo-code SQL suivant :
select * from product where prix > [un chiffre ici]
Afin de mettre cette méthode en action, nous allons créer une méthode testDeRequetes dans notre contrôleur :
@GetMapping(value = "test/produits/{prixLimit}")
public List<Product> testeDeRequetes(@PathVariable int prixLimit)
{
return productDao.findByPrixGreaterThan(400);
}
Rendez-vous à l'adresse http://localhost:9090/test/produits/300. Vous pouvez constater que seuls les produits dont le prix est supérieur à 300 sont retournés.
Si vous souhaitez vérifier la requête générée, par curiosité ou afin de débugger, rendez-vous dans la console. Le fait d'avoir ajouté précédemment spring.jpa.show-sql=true
à notre fichier application.properties permet d'afficher l'ensemble des requêtes, comme illustré ci-après :
Hibernate : select product0_.id as id1_0_, product0_.nom as nom2_0_, product0_.prix as prix3_0_, product0_.prix_achat as prix_ach4_0_ from product product0_ where product0_.prix>?
Implémentez POST, DELETE et PUT
POST
Pour gérer l'ajout de produit, il nous suffit de décommenter la méthode ajouterProduit. Comme save fait partie des requêtes CRUD autogénérées par Spring Data, vous n'avez absolument rien à faire de plus.
Démarrez votre application et rendez-vous dans Postman. Récupérez dans l'historique la requête POST que nous avons utilisée dans la partie précédente du cours, et exécutez-la :
En bas à droite, vous voyez le code 201 vous indiquant que le produit a été créé. Rendez-vous dans la base de données pour vérifier qu'il a bien été ajouté :
Aussi simple que ça !
DELETE
Passons à DELETE. À nouveau, il n'y a rien à faire, à part créer la méthode dans notre contrôleur :
@DeleteMapping (value = "/Produits/{id}")
public void supprimerProduit(@PathVariable int id) {
productDao.deleteById(id);
}
Cette fois, dans Postman, veillez à bien choisir DELETE comme type de requête, comme illustré ci-dessous :
Vous pouvez ensuite vérifier que le produit 1 a bien été supprimé.
PUT
Pour mettre à jour, la méthode est exactement comme pour POST, excepté que le produit passé en paramètre a le même Id qu'un produit existant :
@PutMapping (value = "/Produits")
public void updateProduit(@RequestBody Product product)
{
productDao.save(product);
}
Dans Postman, vous avez :
Voilà, votre produit avec l'Id "1" a été mis à jour dans la base de données.
Écrivez des requêtes à la main
Soyons francs, il est parfois nécessaire d'exécuter des requêtes plus complexes qui ne peuvent pas être générées par ce système de conventions. Pragmatique, Spring nous permet d'écrire une bonne requête artisanale à l'ancienne !
Pour cela, nous allons utiliser la notation JPA (JPQL). En effet, il est hors de question d'utiliser un langage spécifique à une base de données, puisque notre microservice ne doit être couplé à aucun type de base de données.
L'annotation qui permet de le faire est @Query. Regardons un exemple :
@Query("SELECT id, nom, prix FROM Product p WHERE p.prix > :prixLimit")
List<Product> chercherUnProduitCher(@Param("prixLimit") int prix);
En paramètre de l'annotation, vous placez tout simplement votre requête JPQL. L'annotation @Param permet de spécifier le nom du paramètre que vous allez recevoir. Cette annotation est optionnelle. Si elle n'est pas utilisée, l'ordre dans lequel les arguments sont fournis est utilisé. Vous aurez alors une requête de type :
SELECT id, nom, prix FROM Product p WHERE p.prix > ?1
En résumé
Nous avons découvert JPA pour interagir avec notre Database.
Nous avons créé une base de données embarquée avec H2.
Nous allons nous attaquer à la validation des données ! C'est parti pour le prochain chapitre !