Ce chapitre vous permettra de répondre aux problématiques suivantes :
Comment mettre à jour une donnée qui est déjà présente en base de données ?
Comment supprimer une donnée ?
Carlib Assurances, comme de nombreuses entreprises en ligne, a besoin de pouvoir faire évoluer les données de son application.
Il peut s’agir d’un changement induit par un besoin métier : par exemple, le coût des assurances augmente, et il faut donc mettre à jour les produits en base de données. il peut également s’agir d’une correction suite à une erreur lors de la création ou de la modification précédente.
L'évolution d’une donnée peut impliquer soit une modification de cette dernière, soit une suppression.
Indépendamment du contexte qui vous oblige à faire évoluer la donnée, le U pour UPDATE et le D pour DELETE du CRUD sont des fondamentaux à savoir mettre en place.
Mettez à jour les données (UPDATE)
Intéressons-nous tout d’abord à la modification de données. Vous êtes désormais habitué à l’utilisation de Spring Data JPA et à son interface CrudRepository :
Oui, c’est sûr, mais ça ne me dit pas quelle méthode je dois utiliser pour la mise à jour !
Eh oui ! Peut-être que vous vous attendiez à trouver une méthode avec un nom tel que update, mais force est de constater qu’une telle méthode n’existe pas.
Alors, comment faire ?
Le fonctionnement des frameworks ORM est simple. La création et la mise à jour d’un objet sont gérées par les mêmes méthodes save(S entity) et saveAll(Iterable<S> entities).
Mais comment Spring Data JPA fait la différence entre une création et une mise à jour ?
Le framework va inspecter le contenu de l’objet que nous lui fournissons, et pourra identifier s’il est déjà existant en base ou non, grâce à son identifiant unique (la clé primaire).
Notez le code suivant :
package com.openclassrooms.datalayer;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.openclassrooms.datalayer.model.Product;
import com.openclassrooms.datalayer.service.CategoryService;
import com.openclassrooms.datalayer.service.CommentService;
import com.openclassrooms.datalayer.service.ProductService;
@SpringBootApplication
public class DataLayerApplication implements CommandLineRunner {
@Autowired
private ProductService productService;
@Autowired
private CategoryService categoryService;
@Autowired
private CommentService commentService;
public static void main(String[] args) {
SpringApplication.run(DataLayerApplication.class, args);
}
@Override
@Transactional
public void run(String... args) throws Exception {
Product existingProduct = productService.getProductById(1).get();
System.out.println(existingProduct.getCost());
existingProduct.setCost(3000);
productService.addProduct(existingProduct);
existingProduct = productService.getProductById(1).get();
System.out.println(existingProduct.getCost());
}
}
En ajoutant la propriété spring.jpa.show-sql = true dans le fichier application.properties, au sein de la console vous pouvez avoir un visuel de la requête SQL exécutée. Il est intéressant d’observer cette requête :
Hibernate: update produit set cout=?, description=?, nom=? where produit_id=?
Hibernate (l’implémentation JPA utilisée par Spring Data JPA) exécute une mise à jour de toutes les colonnes de la table, alors que nous avons modifié uniquement la valeur du coût. Même les valeurs inchangées ont été mises à jour avec la même valeur !
Quelle importance, me direz-vous peut-être ?! Les performances, bien sûr !
Imaginez que Carlib Assurances complexifie sa structure de base de données, et qu’une table contienne de nombreuses colonnes avec des données denses. Serait-il performant, lors d’une mise à jour, de réinsérer les valeurs de toutes les colonnes ? Absolument pas !
Comment résoudre ce problème, alors ?
Hibernate nous offre la solution grâce à une annotation : @DynamicUpdate.
Cette annotation doit être appliquée sur l’entité (dans notre exemple, la classe Product). Si elle est présente, alors la requête SQL update exécutera un set uniquement sur les valeurs qui ont changé.
J’ai ajouté l’annotation @DynamicUpdate à Product, puis j’ai de nouveau modifié la valeur du coût, et voici le résultat dans la console de la requête :
Hibernate: update produit set cout=? where produit_id=?
Bonne nouvelle, n’est-ce pas ?
Un dernier point : étant donné que la méthode addProduct est utilisée tant dans le cadre d’un ajout que d’une modification, il serait pertinent de la renommer en saveProduct.
Supprimez des données (DELETE)
Intéressons-nous maintenant à la suppression de données. Pas moins de 4 méthodes sont à notre disposition :
delete(S entity)
deleteAll()
deleteAll(Iterable<S> entities)
deleteById(Integer id)
Spring Data JPA tient vraiment à nous faciliter la tâche pour la suppression !
Dans le cas de Carlib Assurances, nous avons créé une relation bidirectionnelle entre Product et Comment. Si vous reprenez le code, vous observez que le type de CASCADE a été défini sur ALL des 2 côtés de la relation :
Côté Product :
@OneToMany(
mappedBy = "produit",
cascade = CascadeType.ALL,
orphanRemoval = true
)
List<Comment> comments = new ArrayList<>();
Côté Comment :
@ManyToOne(
cascade = CascadeType.ALL)
@JoinColumn(name="produit_id")
private Product product;
Dans le cas où on supprime un produit, il est normal de supprimer tous les commentaires associés. Un commentaire sans produit n’a pas de sens, d’un point de vue métier.
Par contre, supprimer un commentaire ne devrait pas entraîner une suppression du produit ! Je pourrais par exemple vouloir supprimer un commentaire qui a un contenu illicite, mais conserver mon produit en base de données.
Comment résoudre ce problème, alors ?
Il nous faut modifier le type de CASCADE. Au lieu d’être permissif et de mettre un CascadeType.ALL, spécifions uniquement les CascadeType nécessaires pour l’insertion et la modification avec CascadeType.PERSIST et CascadeType.MERGE.
Modifiez la classe Comment comme ci-dessous puis testez un deleteById sur un commentaire. Vous noterez que le produit associé n’aura pas été supprimé !
@ManyToOne(
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
}
)
@JoinColumn(name="produit_id")
private Product product;
C’est l’occasion pour moi de vous rappeler que si vous ne spécifiez pas la propriété cascade pour votre relation, alors le comportement par défaut est de ne cascader aucune opération !
À vous de jouer !
Implémentez la suppression par l’ID pour toutes les entités du projet : Product, Catégory et Comment. Vérifiez que les règles de gestion suivantes sont bien suivies pour les CascadeType au niveau des relations :
Supprimer une catégorie ne doit pas supprimer les produits associés.
Supprimer un produit ne doit pas supprimer les catégories associées.
Supprimer un produit doit supprimer les commentaires associés.
Supprimer un commentaire ne doit pas supprimer le produit associé.
Une fois que vous aurez fini, une correction est disponible sur le repository à la branche p3c2.
Vous voulez aller encore plus loins ? Alors, rejoignez-moi dans le dernier chapitre de ce cours pour exécuter des requêtes personnalisées !
En résumé
La modification d’une donnée existante est très similaire à l’insertion d’une nouvelle donnée, et l’annotation @DynamicUpdate peut booster vos performances.
Tenez compte des CascadeType lorsque vous effectuez des suppressions ! Reportez-vous aux règles métiers pour définir si la suppression d’une donnée doit déclencher la suppression d’une autre donnée qui lui est associée.