Aller au contenu principal

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 HTTPURLActionVue
GET/articlesAfficher la listearticles/liste
GET/articles/{id}Afficher le détailarticles/detail
GET/articles/nouveauAfficher le formulaire de créationarticles/nouveau
POST/articles/nouveauSauvegarder l'articleRedirection
GET/articles/{id}/editerAfficher le formulaire d'éditionarticles/editer
POST/articles/{id}/editerMettre à jour l'articleRedirection
POST/articles/{id}/supprimerSupprimer l'articleRedirection
Comparaison avec Symfony

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


Combien de routes composent un CRUD complet dans la convention Spring Boot avec Thymeleaf ?


Pourquoi la suppression utilise-t-elle une requête POST et non DELETE ?


Que fait th:field="*{titre}" dans un formulaire Thymeleaf ?


Pourquoi faut-il un champ caché <input type='hidden' th:field='*{id}'> dans le formulaire d'édition ?


Quelle syntaxe Thymeleaf génère l'URL /articles/42 ?


Dans la méthode POST d'édition, pourquoi écrire article.setId(id) en utilisant l'id de l'URL ?


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.


Bonne pratique - Réutiliser la même vue pour création et édition

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)


Bonne pratique - Toujours utiliser l'id de l'URL pour les mises à jour

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)


Bonne pratique - Confirmation avant suppression

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.

📌 Une solution