🖨️ Version PDF
Dans le projet Wouaf Wouaf, le propriétaire d’un chien est modélisé par Adherent. Dans le discours métier, on dira souvent “propriétaire”. Important : on aligne le cours sur le code : l’entité s’appelle Adherent.
Adherent
Si on stocke un propriétaire comme un simple texte dans Chien :
Chien
Donc on crée une entité : Adherent.
Dans com.ffc.wouaf.model.Adherent :
com.ffc.wouaf.model.Adherent
id : Long
nom : String
adresse : String
telephone : String
email : String
Pour simplifier, j’ai omis certains attributs avec relations.
nom
adresse
telephone
email
Adherent 1 ---- * Chien
Traduction :
ManyToOne
Comme pour Chien, même démarche !
Parce que l’API est un contrat :
Exemple DTO sortie :
public record AdherentDto( Long id, String nom, String adresse, String telephone, String email ) {}
Rôle : Représenter les données renvoyées par l’API (réponse à une requête GET /adherents/1).
Caractéristiques :
DTO création + validation :
public record AdherentCreateDto( @NotBlank String nom, @NotBlank String adresse, @NotBlank String telephone, @Email @NotBlank String email ) {}
Rôle : Représenter les données envoyées à l’API pour créer un nouvel adhérent (ex : requête POST /adherents).
Éviter les fuites de données : Un DTO de sortie peut exposer des champs sensibles ou internes (ex : id), tandis qu’un DTO d’entrée ne doit contenir que ce qui est nécessaire à la création. Validation des entrées : Les annotations comme @NotBlank ou @Email garantissent que les données envoyées par le client sont valides avant d’être traitées.
Responsabilité unique : Chaque DTO a un rôle clair (création vs lecture). Documentation implicite : En voyant AdherentCreateDto, un développeur sait immédiatement qu’il s’agit d’un objet pour la création.
Si tu dois ajouter un champ uniquement pour la création (ex : motDePasse), tu peux le faire dans AdherentCreateDto sans affecter AdherentDto.
Exemple :
public record AdherentCreateDto( @NotBlank String nom, @NotBlank String adresse, @NotBlank String telephone, @Email @NotBlank String email, @Size(min = 8) String motDePasse // Champ spécifique à la création ) {}
Voici comment ces DTOs sont utilisés dans un contrôleur REST
@RestController @RequestMapping("/api/adherents") public class AdherentController { private final AdherentService adherentService; public AdherentController(AdherentService adherentService) { this.adherentService = adherentService; } // Endpoint pour créer un adhérent (utilise AdherentCreateDto) @PostMapping public ResponseEntity<AdherentDto> createAdherent(@Valid @RequestBody AdherentCreateDto createDto) { AdherentDto savedAdherent = adherentService.createAdherent(createDto); return ResponseEntity.ok(savedAdherent); } // Endpoint pour récupérer un adhérent (utilise AdherentDto) @GetMapping("/{id}") public ResponseEntity<AdherentDto> getAdherent(@PathVariable Long id) { AdherentDto adherentDto = adherentService.getAdherentById(id); return ResponseEntity.ok(adherentDto); } }
Dans certains cas simples, un seul DTO peut suffire :
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; public record AdherentUpdateDto( @NotBlank(message = "Le nom ne peut pas être vide") String nom, String adresse, // Optionel : pas de @NotBlank pour permettre les mises à jour partielles String telephone, // Optionel @Email(message = "L'email doit être valide") String email // Optionel mais validé si présent ) {}
Intégration dans le service :
// Méthode pour mettre à jour un adhérent public AdherentDto updateAdherent(Long id, AdherentUpdateDto updateDto) { Adherent adherent = adherentRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("Adhérent non trouvé avec l'ID : " + id)); // Mise à jour conditionnelle des champs (seulement si fournis) if (StringUtils.hasText(updateDto.nom())) { adherent.setNom(updateDto.nom()); } if (StringUtils.hasText(updateDto.adresse())) { adherent.setAdresse(updateDto.adresse()); } if (StringUtils.hasText(updateDto.telephone())) { adherent.setTelephone(updateDto.telephone()); } if (StringUtils.hasText(updateDto.email())) { adherent.setEmail(updateDto.email()); } Adherent updatedAdherent = adherentRepository.save(adherent); return mapToDto(updatedAdherent); }
Le projet contient déjà AdherentRepository. On veut au minimum :
AdherentRepository
public interface AdherentRepository extends JpaRepository<Adherent, Long> { }
Option utile (si vous voulez éviter email doublon) :
boolean existsByEmail(String email);
Règles typiques :
Exemple simple :
@Service public class AdherentService { private final AdherentRepository repo; public AdherentService(AdherentRepository repo) { this.repo = repo; } public AdherentDto create(AdherentCreateDto dto) { Adherent a = new Adherent(); a.setNom(dto.nom()); a.setAdresse(dto.adresse()); a.setTelephone(dto.telephone()); a.setEmail(dto.email()); Adherent saved = repo.save(a); return new AdherentDto(saved.getId(), saved.getNom(), saved.getAdresse(), saved.getTelephone(), saved.getEmail()); } }
Quelques fois j’utilise le mot anglais “Controller” et de temps le mot français, cependant dans le code, il est mieux d’utiliser le mot anglais.
Même si le projet a un contrôleur de type PublicQueryController comme décrit plus haut, on préfère utiliser un controller par fonctionnalité (feature) :
PublicQueryController
GET /api/adherents
POST /api/adherents
GET /api/adherents/{id}
Un controller feature (ou “contrôleur par fonctionnalité”) est une approche d’organisation des contrôleurs dans une application Spring Boot (ou toute application web) qui regroupe les endpoints par domaine métier ou fonctionnalité, plutôt que par type d’opération (comme un PublicQueryController, AdminController, etc.).
Au lieu d’avoir un contrôleur générique qui gère toutes les requêtes publiques (PublicQueryController), on crée un contrôleur spécifique à une entité ou une fonctionnalité (par exemple : AdherentController pour tout ce qui concerne les adhérents). Tout dépend de la façon dont vous voulez gérer les accès par la suite…
AdherentController
ou approche générique qui est moins recommandée mais on la trouve encore dans certaines applications plus anciennes (ou pédagogique) :
Meilleure organisation du code : Chaque contrôleur est dédié à une seule entité ou fonctionnalité, ce qui rend le code plus lisible et maintenable. on prend moins de temps pour effectuer des modifications au bon endroit.
@RestController @RequestMapping("/api/adherents") public class AdherentController { // ensuite on traite que les endpoints pour les adhérents uniquement }
Séparation des responsabilités : Un contrôleur ne fait qu’une seule chose (principe du Single Responsibility) !
Évolutivité : Si on doit ajouter de nouvelles fonctionnalités pour une entité (un endpoint pour rechercher des adhérents par nom), on sait exactement où le mettre (AdherentController). Pas besoin de modifier un contrôleur générique qui risque de devenir de plus en plus complexe.
Clarté des URLs : Les URLs sont cohérentes et prévisibles (et aussi facilités avec des annotations)
/api/adherents
/api/chiens
/api/concours
Meilleure sécurité : On peut appliquer des rôles ou permissions par contrôleur (seul un admin pourra accéder à ConcoursController).
Exemple avec Spring Security (que nous verrons ultérieurement) : @PreAuthorize("hasRole('ADMIN') que nous verrons ultérieurement :
@PreAuthorize("hasRole('ADMIN')
@PreAuthorize("hasRole('ADMIN')") @RestController @RequestMapping("/api/concours") public class ConcoursController { ... }
On pourrait imaginer une classe Contrôleur pour la gestion d’un Adhérent.
@RestController @RequestMapping("/api/adherents") public class AdherentController { private final AdherentService adherentService; public AdherentController(AdherentService adherentService) { this.adherentService = adherentService; } // Récupérer tous les adhérents @GetMapping public ResponseEntity<List<AdherentDto>> getAllAdherents() { List<AdherentDto> adherents = adherentService.getAllAdherents(); return ResponseEntity.ok(adherents); } // Créer un adhérent @PostMapping public ResponseEntity<AdherentDto> createAdherent(@Valid @RequestBody AdherentCreateDto createDto) { AdherentDto savedAdherent = adherentService.createAdherent(createDto); return ResponseEntity.ok(savedAdherent); } // Récupérer un adhérent par ID @GetMapping("/{id}") public ResponseEntity<AdherentDto> getAdherent(@PathVariable Long id) { AdherentDto adherentDto = adherentService.getAdherentById(id); return ResponseEntity.ok(adherentDto); } // Mettre à jour un adhérent @PutMapping("/{id}") public ResponseEntity<AdherentDto> updateAdherent( @PathVariable Long id, @Valid @RequestBody AdherentUpdateDto updateDto) { AdherentDto updatedAdherent = adherentService.updateAdherent(id, updateDto); return ResponseEntity.ok(updatedAdherent); } }
@SpringBootTest @AutoConfigureMockMvc class AdherentApiTest { @Autowired MockMvc mockMvc; @Test void create_adherent_returns_201() throws Exception { mockMvc.perform(post("/api/adherents") .contentType(MediaType.APPLICATION_JSON) .content("{"nom":"Eillish","adresse":"New York","telephone":"0600000000","email":"be@usa.com"}")) .andExpect(status().isCreated()); } }
GET /api/adherents/{id}/chiens