CRUD complet
Notions théoriques
Les 7 routes d'un CRUD
Un CRUD complet (Create, Read, Update, Delete) suit une convention de routes bien établie. Pour l'entité Article :
| Méthode HTTP | URL | Action | Vue |
|---|---|---|---|
| GET | /articles | Afficher la liste | articles/liste |
| GET | /articles/{id} | Afficher le détail | articles/detail |
| GET | /articles/nouveau | Afficher le formulaire de création | articles/nouveau |
| POST | /articles/nouveau | Sauvegarder l'article | Redirection |
| GET | /articles/{id}/editer | Afficher le formulaire d'édition | articles/editer |
| POST | /articles/{id}/editer | Mettre à jour l'article | Redirection |
| POST | /articles/{id}/supprimer | Supprimer l'article | Redirection |
En Symfony, on utilise les méthodes PUT, PATCH et DELETE grâce au composant MethodOverride. Spring Boot avec Thymeleaf fonctionne avec les formulaires HTML classiques qui n'acceptent que GET et POST. La suppression et la modification se font donc toujours en POST.
Thymeleaf pour les formulaires liés à une entité
<!-- th:object lie le formulaire à l'objet "article" placé dans le Model -->
<form th:action="@{/articles/nouveau}" th:object="${article}" method="post">
<!-- th:field="*{titre}" lie le champ à article.titre (getters/setters) -->
<input type="text" th:field="*{titre}" />
<!-- Afficher les erreurs de validation pour un champ -->
<span th:if="${#fields.hasErrors('titre')}" th:errors="*{titre}" class="erreur"></span>
<textarea th:field="*{contenu}"></textarea>
<button type="submit">Enregistrer</button>
</form>
th:field="*{titre}" génère automatiquement les attributs id, name et value basés sur le nom de l'attribut Java.
Liens Thymeleaf avec variables d'URL
<!-- Lien vers la page détail d'un article -->
<a th:href="@{/articles/{id}(id=${article.id})}" th:text="${article.titre}">Titre</a>
<!-- Bouton de suppression (formulaire POST) -->
<form th:action="@{/articles/{id}/supprimer(id=${article.id})}" method="post">
<button type="submit" onclick="return confirm('Supprimer cet article ?')">Supprimer</button>
</form>
<!-- Lien vers le formulaire d'édition -->
<a th:href="@{/articles/{id}/editer(id=${article.id})}">Modifier</a>
Formulaire d'édition : pré-remplir les champs
Pour l'édition, il faut récupérer l'article existant et le passer au modèle. Thymeleaf remplit automatiquement les champs grâce à th:field :
@GetMapping("/{id}/editer")
public String afficherFormulaireEdition(@PathVariable Long id, Model model) {
Article article = articleRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
model.addAttribute("article", article); // th:field lira article.titre, etc.
return "articles/editer";
}
La vue articles/editer.html est identique à articles/nouveau.html, mais le formulaire pointe vers /articles/{id}/editer avec la méthode POST :
<form th:action="@{/articles/{id}/editer(id=${article.id})}" th:object="${article}" method="post">
<!-- Champ caché pour conserver l'id lors de la soumission -->
<input type="hidden" th:field="*{id}" />
<input type="text" th:field="*{titre}" />
<textarea th:field="*{contenu}"></textarea>
<button type="submit">Mettre à jour</button>
</form>
Mise à jour avec save()
articleRepository.save(article) effectue un UPDATE si l'entité possède déjà un id non nul. @ModelAttribute reconstruit l'objet depuis le formulaire, mais l'id doit être présent dans le formulaire (via un champ caché <input type="hidden" th:field="*{id}"/>) pour que JPA reconnaisse l'entité existante.
Exemple pratique
Contrôleur CRUD complet pour Article :
package org.joliciel.monblog.controller;
import jakarta.persistence.EntityNotFoundException;
import org.joliciel.monblog.entity.Article;
import org.joliciel.monblog.repository.ArticleRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
@Controller
@RequestMapping("/articles")
public class ArticleController {
private final ArticleRepository articleRepository;
public ArticleController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
@GetMapping
public String liste(Model model) {
List<Article> articles = articleRepository.findAll();
model.addAttribute("articles", articles);
return "articles/liste";
}
@GetMapping("/{id}")
public String detail(@PathVariable Long id, Model model) {
Article article = articleRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
model.addAttribute("article", article);
return "articles/detail";
}
@GetMapping("/nouveau")
public String nouveau(Model model) {
model.addAttribute("article", new Article());
return "articles/nouveau";
}
@PostMapping("/nouveau")
public String creer(@ModelAttribute Article article,
RedirectAttributes redirectAttributes) {
articleRepository.save(article);
redirectAttributes.addFlashAttribute("message", "Article créé !");
return "redirect:/articles";
}
@GetMapping("/{id}/editer")
public String editer(@PathVariable Long id, Model model) {
Article article = articleRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
model.addAttribute("article", article);
return "articles/editer";
}
@PostMapping("/{id}/editer")
public String mettreAJour(@PathVariable Long id,
@ModelAttribute Article article,
RedirectAttributes redirectAttributes) {
article.setId(id); // Sécurité : utiliser l'id de l'URL, pas du formulaire
articleRepository.save(article);
redirectAttributes.addFlashAttribute("message", "Article mis à jour !");
return "redirect:/articles/" + id;
}
@PostMapping("/{id}/supprimer")
public String supprimer(@PathVariable Long id,
RedirectAttributes redirectAttributes) {
articleRepository.deleteById(id);
redirectAttributes.addFlashAttribute("message", "Article supprimé.");
return "redirect:/articles";
}
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Dans ce TP, vous allez compléter le CRUD Article dans le projet MonBlog.
Étape 1 — Implémenter l'édition (GET)
Ajoutez la méthode qui affiche le formulaire d'édition pré-rempli.
Les formulaires de création et d'édition partagent souvent la même structure HTML. Vous pouvez utiliser un seul template articles/formulaire.html et adapter l'action du formulaire avec th:action="${article.id != null ? @{/articles/{id}/editer(id=${article.id})} : @{/articles/nouveau}}".
Étape 2 — Implémenter la mise à jour (POST)
N'utilisez jamais l'id provenant du formulaire pour déterminer quelle entité modifier (article.getId()). Utilisez toujours l'id extrait de l'URL (@PathVariable Long id). Cela empêche un utilisateur malveillant de modifier l'id dans le formulaire pour écraser l'article d'un autre utilisateur.
Étape 3 — Implémenter la suppression (POST)
Ajoutez toujours une confirmation JavaScript avant la suppression pour éviter les suppressions accidentelles : <form ... onsubmit="return confirm('Supprimer définitivement cet article ?')">. En production, préférez un modal Bootstrap à confirm() pour une meilleure expérience utilisateur.