Aller au contenu

Feature – Chien (Partie 1 : création Entity, DTO et MapStruct)

1. Problème réel

Dans une application métier, l’objet principal (ici Chien) concentre :

Si on code trop vite :

Objectif : construire une feature propre dès le départ comme nous le ferons aussi pour l’adhérent (propriétaire)


2. UML (diagramme de classes)

`Race 1 ---- * Chien`
`Proprietaire 1 ---- * Chien`

Le chien est associé à une race et un propriétaire (ManyToOne).


3. Entité JPA : Chien

Pour le moment, je n’utilise pas le plugin Lombok mais on pourrait l’utiliser par la suite pour alléger notre code.

Explications :

@Entity
public class Chien {

  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable=false, unique=true)
  private String numeroTatouage;

  @Column(nullable=false)
  private String nom;

  @Enumerated(EnumType.STRING)
  @Column(nullable=false)
  private EtatChien etat;

  @ManyToOne(fetch = FetchType.LAZY, optional=false)
  private Race race;

  @ManyToOne(fetch = FetchType.LAZY, optional=false)
  private Adherent proprietaire;

  // getters/setters
}

Enum (ou une classe) :

public enum EtatChien {
  INSCRIT, RETIRE
}

4. DTO : pourquoi et comment ?

On veut :

public record ChienCreateDto(
  @NotBlank String numeroTatouage,
  @NotBlank String nom,
  @NotNull Long raceId,
  @NotNull Long proprietaireId
) {}

DTO de sortie (pensé pour l’UI) :

public record ChienDto(
  Long id,
  String numeroTatouage,
  String nom,
  String etat,
  Long raceId,
  String raceCode,
  String raceNom,
  Long proprietaireId,
  String proprietaireNom
) {}

5. MapStruct : pourquoi un mapper ?

Avant, il faut installer les dépendances MapStruct dans votre fichier pom.xml. J’ai aussi mis Lombok.

Sans mapper :

Avec MapStruct :

Vous voyez qu’ici on donne des noms différents aux attributs qui seront retournés au front dans un format JSON.

@Mapper(componentModel = "spring")
public interface ChienMapper {

  @Mapping(target="etat", expression="java(chien.getEtat().name())") // vous ne l'avez pas dans la première version, à enlever !
  @Mapping(target="raceId", source="race.id")
  @Mapping(target="raceCode", source="race.code")
  @Mapping(target="raceNom", source="race.nom")
  @Mapping(target="proprietaireId", source="proprietaire.id")
  @Mapping(target="proprietaireNom",
           expression="java(chien.getProprietaire().getPrenom() + " " + chien.getProprietaire().getNom())")
  ChienDto toDto(Chien chien);
}

6. Repository

Pas de changement !

public interface ChienRepository extends JpaRepository<Chien, Long> {
  boolean existsByNumeroTatouage(String numeroTatouage);
}

7. Service (création)

Objectifs :

@Service
public class ChienService {

  private final ChienRepository chienRepository;
  private final RaceRepository raceRepository;
  private final AdherentRepository adherentRepository;
  private final ChienMapper mapper;

  public ChienService(ChienRepository chienRepository, RaceRepository raceRepository,
                      AdherentRepository adherentRepository, ChienMapper mapper) {
    this.chienRepository = chienRepo;
    this.raceRepository = raceRepo;
    this.adherentRepository = adherentRepository; // qui est le propriétaire dans Chien
    this.mapper = mapper;
  }

  @Transactional
  public ChienDto create(ChienCreateDto dto) {

    if (chienRepository.existsByNumeroTatouage(dto.numeroTatouage())) {
      throw new IllegalArgumentException("Tatouage déjà utilisé : " + dto.numeroTatouage());
    }

    Race race = raceRepository.findById(dto.raceId())
      .orElseThrow(() -> new IllegalArgumentException("Race inconnue id=" + dto.raceId()));

    Adherent p = adherentRepository.findById(dto.proprietaireId())
      .orElseThrow(() -> new IllegalArgumentException("Propriétaire inconnu id=" + dto.proprietaireId()));

    Chien c = new Chien();
    c.setNumeroTatouage(dto.numeroTatouage());
    c.setNom(dto.nom());
    c.setEtat(EtatChien.INSCRIT);
    c.setRace(race);
    c.setProprietaire(p);

    return mapper.toDto(chienRepository.save(c));
  }
}

8. Controller

Première version

@RestController
@RequestMapping("/api/chiens")
public class ChienController {

  private final ChienService chienService;

  public ChienController(ChienService chienService)
  {
    this.chienService = chienService;
  }

  @PostMapping
  public ResponseEntity<ChienDto> create(@Valid @RequestBody ChienCreateDto createDto)
  {
    return ResponseEntity.status(HttpStatus.CREATED).body(chienService.create(createDto));
  }
}

Seconde version

@RestController
@RequestMapping("/api/chiens")
public class ChienController {

    private final ChienService chienService;

    public ChienController(ChienService chienService) {
        this.chienService = chienService;
    }

    // Récupérer tous les chiens : GET /api/chiens
    @GetMapping
    public ResponseEntity<List<ChienDto>> getAllChiens() {
        List<ChienDto> chiens = chienService.getAllChiens();
        return ResponseEntity.ok(chiens);
    }

    // Créer un chien : POST /api/chiens
    @PostMapping
    public ResponseEntity<ChienDto> createChien(@Valid @RequestBody ChienCreateDto createDto) {
        ChienDto savedChien = chienService.createChien(createDto);
        return ResponseEntity.ok(savedChien);
    }

    // Récupérer un chien par ID : GET /api/chiens/{id}
    @GetMapping("/{id}")
    public ResponseEntity<ChienDto> getChien(@PathVariable Long id) {
        ChienDto chienDto = chienService.getChienById(id);
        return ResponseEntity.ok(chienDto);
    }
}

Remarques :

On utilise HttpStatus.CREATED (201) : C’est le statut HTTP recommandé pour indiquer qu’une ressource a été créée avec succès. Le code 201 est semantiquement correct pour une opération POST (création).

Dans la version du contrôleur ci-dessous, on utilise une autre syntaxe : return ResponseEntity.ok(savedChien);

On utilise ResponseEntity.ok(), qui correspond à HttpStatus.OK (200). Le code 200 signifie que la requête a réussi, mais il est moins précis qu’un 201 pour une création.

Version 1 : Le résultat de chienService.create(dto) est directement retourné sans être stocké dans une variable. Moins pratique si on veut ajouter des logs ou des traitements intermédiaires.

Version 2 : Le résultat est stocké dans savedChien ce qui permettrait d’ajouter des logs ou des traitements avant de retourner la réponse.

Exemple :

ChienDto savedChien = chienService.createChien(createDto);
log.info("Chien créé avec succès : {}", savedChien);
return ResponseEntity.ok(savedChien);

Le code idéal serait celui-ci :

@PostMapping
public ResponseEntity<ChienDto> createChien(@Valid @RequestBody ChienCreateDto createDto) {
    ChienDto savedChien = chienService.createChien(createDto);
	log.info("Chien créé avec succès : {}", savedChien);
    return ResponseEntity.status(HttpStatus.CREATED).body(savedChien); // retourne 201 pour création
}

Conclusion :

Exemple de structure

├── controller/
│   ├── AdherentController.java
│   ├── ChienController.java
│   ├── ConcoursController.java
│   ├── RaceController.java
│   └── EpreuveController.java
│   └── ...
├── service/
│   ├── AdherentService.java
│   ├── ChienService.java
│   └── ...
├── dto/
│   ├── AdherentDto.java
│   ├── AdherentCreateDto.java
│   ├── AdherentUpdateDto.java
│   ├── ChienDto.java
│   ├── ChienCreateDto.java
│   ├── ChienUpdateDto.java
│   └── ...
└── ...

9. Exemples HTTP (linux)

curl -X POST http://localhost:8080/api/chiens \
  -H "Content-Type: application/json" \
  -d '{"numeroTatouage":"T123","nom":"Rex","raceId":1,"proprietaireId":1}'

Sinon, utilisez Swagger


10. Tests conseillés


11. Pièges


12. Mini-exercices

  1. Ajoutez un endpoint GET /api/chiens/{id} (pensez à ajouter @PathVariable avant le paramètre Long id dans le contrôleur)
  2. Ajoutez une règle : nom minimal 2 caractères.