Aller au contenu

Race (référentiel propre et sans doublons)

Problème réel

Si on laisse les utilisateurs saisir la race en texte libre :

Solution : une table Race (référentiel) + un identifiant court (code).


Objectifs


UML (avant de coder)

Diagramme de classes

Traduction :

Diagramme de séquence (création)

Client → RaceController → RaceService → RaceRepository → DB


Implémentation pas-à-pas

Entité JPA (ce qui sera stocké en base)

Points pédagogiques :

@Entity
public class Race {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

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

  @Column(nullable = false)
  private String nom;

  // getters/setters
}

Pourquoi unique=true ?


Repository (accès données)

Le repository donne des méthodes :

public interface RaceRepository extends JpaRepository<Race, Long> {
  boolean existsByCode(String code);
}

DTO : ne pas exposer l’entité

Pourquoi DTO ?

Dans notre exemple, il n’y a pas d’élément sensible, mais pour d’autres Entités, ça sera le cas.

Attention, il faudra un fichier pour chaque Record

Dans RaceDto :

public record RaceDto(Long id, String code, String nom) {}

Dans RaceCreateDto.java :

public record RaceCreateDto(
  @NotBlank(message="Le code est obligatoire")
  @Size(min=2, max=10, message="Le code doit faire 2 à 10 caractères")
  String code,

  @NotBlank(message="Le nom est obligatoire")
  String nom
) {}

Service : là où vivent les règles…

Règle : code unique.

Pourquoi ici ?

@Service
public class RaceService {
  private final RaceRepository raceRepository;

  public RaceService(RaceRepository raceRepository) {
    this.raceRepository = raceRepository;
  }

  public List<RaceDto> list() {
    return repo.findAll().stream()
      .map(r -> new RaceDto(r.getId(), r.getCode(), r.getNom()))
      .toList();
  }

  public RaceDto create(RaceCreateDto dto) {
    if (raceRepository.existsByCode(dto.code())) {
      throw new IllegalArgumentException("Le code de race existe déjà : " + dto.code());
    }
    Race r = new Race();
    r.setCode(dto.code());
    r.setNom(dto.nom());
    Race saved = raceRepository.save(r);
    return new RaceDto(saved.getId(), saved.getCode(), saved.getNom());
  }
}

Controller : la couche HTTP

Rôle du controller :

@RestController
@RequestMapping("/api/races")
public class RaceController {

  private final RaceService raceService;

  public RaceController(RaceService raceService) { this.raceService = raceService; }

  @GetMapping
  public List<RaceDto> list() { return raceService.list(); }

  @PostMapping
  public ResponseEntity<RaceDto> create(@Valid @RequestBody RaceCreateDto dto) {
    return ResponseEntity.status(HttpStatus.CREATED).body(raceService.create(dto));
  }
}

Exemples HTTP

via Swagger ou navigateur :

ou commande (linux) :

curl -X POST http://localhost:8080/api/races \
  -H "Content-Type: application/json" \
  -d '{"code":"BA","nom":"Berger Australien"}'

Tests (unitaire ultérieurement)

On teste la règle code unique.

@ExtendWith(MockitoExtension.class)
class RaceServiceTest {

  @Mock RaceRepository repo;
  @InjectMocks RaceService service;

  @Test
  void should_refuse_duplicate_code() {
    when(repo.existsByCode("BA")).thenReturn(true);

    var dto = new RaceCreateDto("BA", "Berger Australien");

    assertThrows(IllegalArgumentException.class, () -> service.create(dto));
  }
}

Pièges fréquents


8. Mini-exercices

  1. Ajoutez un endpoint GET /api/races/{id}.
  2. Ajoutez un test : si la race n’existe pas alors 404