Aller au contenu

Feature 03 — Adhérent (propriétaire dans le métier)

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.


1) Pourquoi une entité Adherent ?

Si on stocke un propriétaire comme un simple texte dans Chien :

Donc on crée une entité : Adherent.


2) Ce que dit le code (attributs réels)

Dans com.ffc.wouaf.model.Adherent :

Pour simplifier, j’ai omis certains attributs avec relations.

À quoi servent ces champs ?


3) UML (diagramme de classes)

Adherent 1 ---- * Chien

Traduction :


4) DTO : décider ce qu’on expose

Comme pour Chien, même démarche !

Pourquoi DTO ?

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).

Caractéristiques :


Pourquoi cette séparation ?

Sécurité

É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.

Clarté du code

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.

Évolutivité

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
) {}

Adaptation aux besoins métiers


Exemple Complet avec un Contrôleur Spring Boot

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);
    }
}

Quand utiliser un seul DTO ?

Dans certains cas simples, un seul DTO peut suffire :

DTO de Mise à Jour (AdherentUpdateDto)

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);
    }

5) Repository (Spring Data JPA)

Le projet contient déjà AdherentRepository. On veut au minimum :

public interface AdherentRepository extends JpaRepository<Adherent, Long> {
}

Option utile (si vous voulez éviter email doublon) :

boolean existsByEmail(String email);

6) Service : Règles métier

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());
  }
}

7) Contrôleur : endpoints dédiés (plus propre)

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) :

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.).

Qu’est-ce que ça signifie concrètement ?

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…

Approche feature

ou approche générique qui est moins recommandée mais on la trouve encore dans certaines applications plus anciennes (ou pédagogique) :

Pourquoi préférer un controller feature ?

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.

Exemple :

@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) !

Exemple :

É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.

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')")
@RestController
@RequestMapping("/api/concours")
public class ConcoursController { ... }
Exemple AdherentController

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);
    }
}

8) Petit test d’intégration

@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());
  }
}

9) A Faire

  1. Ajoutez une contrainte email unique (service + BD si possible).
  2. Ajoutez un endpoint GET /api/adherents/{id}/chiens (prépare la feature Chien) pour afficher les chiens de l’adhérent selon l’Id.