Pular para o conteúdo principal

TP3.1 - Architecture en Couches et API CRUD Complète

Application de Gestion des Étudiants​

Objectifs​

Ce travail pratique vous permettra de:

  • Comprendre l'architecture en couches (Controller → Service → Repository)
  • Créer une API REST CRUD complète
  • Implémenter les opérations Create, Read, Update, Delete
  • Utiliser Spring Data JPA avec H2 (base en mémoire)
  • Tester une API REST avec Postman ou un navigateur
  • Gérer les erreurs et les validations

Table des Matières​

  1. Architecture en Couches
  2. Création du Projet
  3. Création de l'Entité Student
  4. Création du Repository
  5. Création du Service
  6. Création du Controller
  7. Configuration H2
  8. Tests des Endpoints
  9. Exercices Supplémentaires

Ressources​

Documentation avec captures d'écran détaillées:

📸 Références du TP3.1 - Google Drive


1. Architecture en Couches​

Vue d'Ensemble​

L'architecture en couches sépare les responsabilités en 4 niveaux distincts:

+----------------+       +----------------+       +----------------+       +----------------+
| | | | | | | |
| Controller +------>+ Service +------>+ Repository +------>+ Database |
| | | | | | | |
+----------------+ +----------------+ +----------------+ +----------------+
(HTTP) (Logique Métier) (Accès Données) (Stockage)

Diagramme Mermaid​

Responsabilités de Chaque Couche​

1. Controller (Couche de Présentation)​

Rôle:

  • Point d'entrée de l'application
  • Reçoit les requêtes HTTP (GET, POST, PUT, DELETE)
  • Analyse les paramètres de la requête
  • Appelle le Service approprié
  • Retourne la réponse au client (JSON)

Annotations:

  • @RestController
  • @RequestMapping
  • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping

Exemple:

@RestController
@RequestMapping("/api/students")
public class StudentController {

@Autowired
private StudentService studentService;

@GetMapping
public List<Student> getAllStudents() {
return studentService.getAllStudents();
}
}

2. Service (Couche Métier)​

Rôle:

  • Contient la logique métier de l'application
  • Intermédiaire entre Controller et Repository
  • Applique les règles de validation
  • Effectue les calculs et transformations
  • Gère les transactions

Annotations:

  • @Service
  • @Transactional (pour les transactions)

Exemple:

@Service
public class StudentService {

@Autowired
private StudentRepository studentRepository;

public List<Student> getAllStudents() {
return studentRepository.findAll();
}
}

3. Repository (Couche d'Accès aux Données)​

Rôle:

  • Interagit directement avec la base de données
  • Effectue les opérations CRUD
  • Utilise Spring Data JPA
  • Génération automatique des requêtes SQL

Annotations:

  • @Repository
  • Interface qui étend JpaRepository

Exemple:

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
// Méthodes générées automatiquement par Spring Data JPA
}

4. Database (Couche de Persistance)​

Rôle:

  • Stockage physique des données
  • Gestion des transactions ACID
  • Indexation et optimisation

Technologies:

  • H2 (base en mémoire pour développement/tests)
  • PostgreSQL, MySQL (production)

Flux d'une Requête​


2. Création du Projet​

2.1 Spring Initializr​

Allez sur https://start.spring.io/

Configuration:

ParamètreValeur
ProjectMaven
LanguageJava
Spring Boot3.2.0 (ou dernière version stable)
Groupcom.example
Artifactstudent-management
Namestudent-management
Package namecom.example.student
PackagingJar
Java17

Dépendances à ajouter:

  1. Spring Web - Pour créer l'API REST
  2. Spring Data JPA - Pour la persistance
  3. H2 Database - Base de données en mémoire
  4. Validation - Pour valider les données
  5. Spring Boot DevTools - Pour le rechargement automatique

2.2 Import dans VS Code​

  1. Téléchargez le projet (ZIP)
  2. Décompressez dans votre workspace
  3. Ouvrez VS Code
  4. File → Open Folder → Sélectionnez student-management
  5. Attendez que Maven télécharge les dépendances

2.3 Structure du Projet​

Créez la structure de packages suivante:

src/main/java/com/example/student/
├── StudentManagementApplication.java
├── controller/
│ └── StudentController.java
├── service/
│ └── StudentService.java
├── repository/
│ └── StudentRepository.java
├── model/
│ └── Student.java
└── exception/
└── ResourceNotFoundException.java

3. Création de l'Entité Student​

3.1 Classe Student​

Créez src/main/java/com/example/student/model/Student.java:

package com.example.student.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

@Entity
@Table(name = "students")
public class Student {

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

@NotBlank(message = "Le prénom est obligatoire")
@Size(min = 2, max = 50, message = "Le prénom doit contenir entre 2 et 50 caractères")
@Column(name = "first_name", nullable = false)
private String firstName;

@NotBlank(message = "Le nom est obligatoire")
@Size(min = 2, max = 50, message = "Le nom doit contenir entre 2 et 50 caractères")
@Column(name = "last_name", nullable = false)
private String lastName;

@NotBlank(message = "L'email est obligatoire")
@Email(message = "Format d'email invalide")
@Column(name = "email", nullable = false, unique = true)
private String email;

@Column(name = "age")
private Integer age;

// Constructeur par défaut (requis par JPA)
public Student() {
}

// Constructeur avec paramètres
public Student(String firstName, String lastName, String email, Integer age) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.age = age;
}

// Getters et Setters
public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
}

3.2 Explication des Annotations​

AnnotationRôle
@EntityMarque la classe comme entité JPA
@Table(name = "students")Nom de la table SQL
@IdClé primaire
@GeneratedValue(strategy = GenerationType.IDENTITY)Auto-incrémentation
@ColumnConfiguration de la colonne
@NotBlankValidation: champ non vide
@SizeValidation: taille min/max
@EmailValidation: format email

4. Création du Repository​

4.1 Interface StudentRepository​

Créez src/main/java/com/example/student/repository/StudentRepository.java:

package com.example.student.repository;

import com.example.student.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

// Spring Data JPA génère automatiquement l'implémentation

// Méthodes personnalisées (optionnelles)
Optional<Student> findByEmail(String email);

boolean existsByEmail(String email);
}

4.2 Méthodes Disponibles​

Méthodes héritées de JpaRepository:

MéthodeDescription
save(Student s)Sauvegarder ou mettre à jour
findAll()Récupérer tous les étudiants
findById(Long id)Récupérer par ID
deleteById(Long id)Supprimer par ID
count()Compter les enregistrements
existsById(Long id)Vérifier l'existence

Méthodes personnalisées:

  • findByEmail(String email) - Trouver par email
  • existsByEmail(String email) - Vérifier si l'email existe

Note: Spring Data JPA génère automatiquement les requêtes SQL basées sur le nom de la méthode!


5. Création du Service​

5.1 Classe StudentService​

Créez src/main/java/com/example/student/service/StudentService.java:

package com.example.student.service;

import com.example.student.model.Student;
import com.example.student.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class StudentService {

@Autowired
private StudentRepository studentRepository;

// CREATE - Créer un étudiant
public Student createStudent(Student student) {
// Vérifier si l'email existe déjà
if (studentRepository.existsByEmail(student.getEmail())) {
throw new RuntimeException("Un étudiant avec cet email existe déjà");
}
return studentRepository.save(student);
}

// READ - Récupérer tous les étudiants
public List<Student> getAllStudents() {
return studentRepository.findAll();
}

// READ - Récupérer un étudiant par ID
public Optional<Student> getStudentById(Long id) {
return studentRepository.findById(id);
}

// UPDATE - Mettre à jour un étudiant
public Student updateStudent(Long id, Student studentDetails) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Étudiant non trouvé avec l'id: " + id));

student.setFirstName(studentDetails.getFirstName());
student.setLastName(studentDetails.getLastName());
student.setEmail(studentDetails.getEmail());
student.setAge(studentDetails.getAge());

return studentRepository.save(student);
}

// DELETE - Supprimer un étudiant
public void deleteStudent(Long id) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Étudiant non trouvé avec l'id: " + id));

studentRepository.delete(student);
}

// Méthode utilitaire - Trouver par email
public Optional<Student> getStudentByEmail(String email) {
return studentRepository.findByEmail(email);
}
}

5.2 Logique Métier​

Le Service contient:

  1. Validation: Vérifier si l'email existe avant création
  2. Gestion d'erreurs: Lancer des exceptions si étudiant non trouvé
  3. Orchestration: Coordonner les appels au Repository
  4. Transformation: Mapper les données si nécessaire

6. Création du Controller​

6.1 Classe StudentController​

Créez src/main/java/com/example/student/controller/StudentController.java:

package com.example.student.controller;

import com.example.student.model.Student;
import com.example.student.service.StudentService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/students")
public class StudentController {

@Autowired
private StudentService studentService;

// CREATE - POST /api/students
@PostMapping
public ResponseEntity<Student> createStudent(@Valid @RequestBody Student student) {
try {
Student createdStudent = studentService.createStudent(student);
return new ResponseEntity<>(createdStudent, HttpStatus.CREATED);
} catch (RuntimeException e) {
return new ResponseEntity<>(null, HttpStatus.CONFLICT);
}
}

// READ ALL - GET /api/students
@GetMapping
public ResponseEntity<List<Student>> getAllStudents() {
List<Student> students = studentService.getAllStudents();
return new ResponseEntity<>(students, HttpStatus.OK);
}

// READ ONE - GET /api/students/{id}
@GetMapping("/{id}")
public ResponseEntity<Student> getStudentById(@PathVariable Long id) {
return studentService.getStudentById(id)
.map(student -> new ResponseEntity<>(student, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}

// UPDATE - PUT /api/students/{id}
@PutMapping("/{id}")
public ResponseEntity<Student> updateStudent(
@PathVariable Long id,
@Valid @RequestBody Student studentDetails) {
try {
Student updatedStudent = studentService.updateStudent(id, studentDetails);
return new ResponseEntity<>(updatedStudent, HttpStatus.OK);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}

// DELETE - DELETE /api/students/{id}
@DeleteMapping("/{id}")
public ResponseEntity<HttpStatus> deleteStudent(@PathVariable Long id) {
try {
studentService.deleteStudent(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}

// SEARCH BY EMAIL - GET /api/students/email/{email}
@GetMapping("/email/{email}")
public ResponseEntity<Student> getStudentByEmail(@PathVariable String email) {
return studentService.getStudentByEmail(email)
.map(student -> new ResponseEntity<>(student, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
}

6.2 Endpoints REST​

Méthode HTTPEndpointActionCode Succès
POST/api/studentsCréer un étudiant201 CREATED
GET/api/studentsLister tous les étudiants200 OK
GET/api/students/\{id\}Récupérer un étudiant200 OK
PUT/api/students/\{id\}Mettre à jour un étudiant200 OK
DELETE/api/students/\{id\}Supprimer un étudiant204 NO CONTENT
GET/api/students/email/\{email\}Chercher par email200 OK

6.3 Codes de Statut HTTP​


7. Configuration H2​

7.1 Fichier application.properties​

Créez/modifiez src/main/resources/application.properties:

# Port du serveur
server.port=8080

# Configuration H2 Database
spring.datasource.url=jdbc:h2:mem:studentdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# Configuration JPA
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Console H2 (accessible via navigateur)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

7.2 Accès à la Console H2​

Une fois l'application démarrée, accédez à:

http://localhost:8080/h2-console

Paramètres de connexion:

ChampValeur
JDBC URLjdbc:h2:mem:studentdb
User Namesa
Password(laisser vide)

8. Tests des Endpoints​

8.1 Démarrage de l'Application​

Dans VS Code:

mvn clean install -DskipTests spring-boot:run

Logs attendus:

Hibernate: create table students (...)
Started StudentManagementApplication in X.XXX seconds

8.2 Tests avec le Navigateur​

Test 1: Créer un étudiant (POST)​

Note: Le navigateur ne peut pas faire de POST facilement. Utilisez Postman ou curl.

Avec curl (PowerShell/CMD):

curl -X POST http://localhost:8080/api/students ^
-H "Content-Type: application/json" ^
-d "{\"firstName\":\"Jean\",\"lastName\":\"Dupont\",\"email\":\"jean.dupont@example.com\",\"age\":20}"

Test 2: Lister tous les étudiants (GET)​

Dans le navigateur:

http://localhost:8080/api/students

Réponse attendue:

[
{
"id": 1,
"firstName": "Jean",
"lastName": "Dupont",
"email": "jean.dupont@example.com",
"age": 20
}
]

Test 3: Récupérer un étudiant par ID (GET)​

http://localhost:8080/api/students/1

Réponse attendue:

{
"id": 1,
"firstName": "Jean",
"lastName": "Dupont",
"email": "jean.dupont@example.com",
"age": 20
}

8.3 Tests avec Postman​

Installation de Postman​

  1. Téléchargez Postman: https://www.postman.com/downloads/
  2. Installez et lancez Postman

Collection de Tests​

Test 1: CREATE (POST)

  • Méthode: POST
  • URL: http://localhost:8080/api/students
  • Headers: Content-Type: application/json
  • Body (raw JSON):
{
"firstName": "Alice",
"lastName": "Martin",
"email": "alice.martin@example.com",
"age": 22
}

Résultat attendu: Status 201 CREATED

Test 2: READ ALL (GET)

  • Méthode: GET
  • URL: http://localhost:8080/api/students

Résultat attendu: Status 200 OK + Liste d'étudiants

Test 3: READ ONE (GET)

  • Méthode: GET
  • URL: http://localhost:8080/api/students/1

Résultat attendu: Status 200 OK + Détails de l'étudiant

Test 4: UPDATE (PUT)

  • Méthode: PUT
  • URL: http://localhost:8080/api/students/1
  • Headers: Content-Type: application/json
  • Body (raw JSON):
{
"firstName": "Alice",
"lastName": "Martin-Modifié",
"email": "alice.martin.updated@example.com",
"age": 23
}

Résultat attendu: Status 200 OK + Étudiant mis à jour

Test 5: DELETE (DELETE)

  • Méthode: DELETE
  • URL: http://localhost:8080/api/students/1

Résultat attendu: Status 204 NO CONTENT

Test 6: SEARCH BY EMAIL (GET)

  • Méthode: GET
  • URL: http://localhost:8080/api/students/email/alice.martin@example.com

Résultat attendu: Status 200 OK + Étudiant trouvé

8.4 Vérification dans la Console H2​

  1. Accédez à http://localhost:8080/h2-console
  2. Connectez-vous
  3. Exécutez la requête:
SELECT * FROM students;

Vous devriez voir tous les étudiants créés.


9. Exercices Supplémentaires​

Exercice 1: Ajouter un Champ "Programme"​

Objectif: Ajouter un champ program (String) à l'entité Student

Instructions:

  1. Modifiez la classe Student.java
  2. Ajoutez le champ program
  3. Générez les getters/setters
  4. Testez la création d'un étudiant avec un programme

Exercice 2: Recherche par Nom​

Objectif: Implémenter une recherche par nom de famille

Instructions:

  1. Ajoutez une méthode dans StudentRepository:
    List<Student> findByLastName(String lastName);
  2. Ajoutez une méthode dans StudentService
  3. Ajoutez un endpoint dans StudentController:
    @GetMapping("/lastname/\{lastName\}")
  4. Testez avec Postman

Exercice 3: Pagination​

Objectif: Ajouter la pagination à la liste des étudiants

Instructions:

  1. Modifiez la méthode getAllStudents() dans le Service:
    public Page<Student> getAllStudents(int page, int size) {
    Pageable pageable = PageRequest.of(page, size);
    return studentRepository.findAll(pageable);
    }
  2. Modifiez le Controller pour accepter les paramètres page et size
  3. Testez: http://localhost:8080/api/students?page=0&size=5

Exercice 4: Gestion des Erreurs Personnalisée​

Objectif: Créer une gestion d'erreurs globale

Instructions:

  1. Créez ResourceNotFoundException.java:
package com.example.student.exception;

public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
  1. Créez GlobalExceptionHandler.java:
package com.example.student.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
  1. Utilisez ResourceNotFoundException dans le Service
  2. Testez en essayant d'accéder à un étudiant inexistant

Exercice 5: Validation Avancée​

Objectif: Ajouter des validations personnalisées

Instructions:

  1. Ajoutez des contraintes dans Student.java:
    @Min(value = 18, message = "L'âge minimum est 18 ans")
    @Max(value = 100, message = "L'âge maximum est 100 ans")
    private Integer age;
  2. Testez en créant un étudiant avec un âge invalide
  3. Observez le code 400 BAD REQUEST

Exercice 6: Migration vers PostgreSQL​

Objectif: Remplacer H2 par PostgreSQL

Instructions:

  1. Ajoutez la dépendance PostgreSQL dans pom.xml
  2. Modifiez application.properties:
    spring.datasource.url=jdbc:postgresql://localhost:5432/studentdb
    spring.datasource.username=postgres
    spring.datasource.password=postgres
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
    spring.jpa.hibernate.ddl-auto=update
  3. Créez la base de données studentdb dans PostgreSQL
  4. Relancez l'application
  5. Vérifiez que les données persistent après redémarrage

Résolution de Problèmes​

Problème 1: Erreur 404 sur tous les endpoints​

Cause: Le Controller n'est pas scanné par Spring

Solution:

Vérifiez que StudentController est dans le même package (ou sous-package) que StudentManagementApplication.

Problème 2: Erreur de validation​

Symptôme:

Validation failed for object='student'

Solution:

Vérifiez que les données envoyées respectent les contraintes @NotBlank, @Email, @Size, etc.

Problème 3: Console H2 non accessible​

Symptôme:

404 on /h2-console

Solution:

Vérifiez dans application.properties:

spring.h2.console.enabled=true

Problème 4: Conflit d'email​

Symptôme:

Status 409 CONFLICT

Solution:

Normal! Un étudiant avec cet email existe déjà. C'est la validation qui fonctionne.


Questions de Révision​

  1. Quelle est la responsabilité de chaque couche (Controller, Service, Repository)?

  2. Pourquoi séparer le code en couches?

  3. Quelle est la différence entre @RestController et @Service?

  4. Que fait l'annotation @Valid dans le Controller?

  5. Comment Spring Data JPA génère-t-il les requêtes SQL?

  6. Quelle est la différence entre save() pour créer et pour mettre à jour?

  7. Pourquoi utiliser Optional<Student> dans le Service?

  8. Quel code HTTP retourner après une suppression réussie?


Livrables​

À remettre:

  1. Code source complet du projet
  2. Captures d'écran Postman montrant les 6 tests réussis
  3. Capture d'écran H2 Console avec des données
  4. Au moins 2 exercices complétés
  5. Document de réponses aux questions de révision

Critères d'Évaluation​

CritèrePoints
Architecture en couches correctement implémentée20%
Entité Student avec validations15%
Repository fonctionnel10%
Service avec logique métier20%
Controller avec tous les endpoints CRUD20%
Tests réussis (Postman/Browser)10%
Exercices supplémentaires5%
Total100%

Résumé​

Ce que vous avez appris​

  • Architecture en couches (Controller → Service → Repository → Database)
  • Création d'une API REST CRUD complète
  • Opérations CRUD: Create, Read, Update, Delete
  • Validation des données avec Bean Validation
  • Gestion des codes de statut HTTP
  • Utilisation de H2 pour le développement
  • Tests d'API avec Postman

Bonnes Pratiques​

  • Séparer les responsabilités en couches distinctes
  • Valider les données à l'entrée (Controller)
  • Gérer les erreurs proprement (exceptions personnalisées)
  • Utiliser les codes HTTP appropriés
  • Tester tous les endpoints
  • Documenter l'API

Prochaines Étapes​

Dans les prochains TPs, nous verrons:

  • TP4: Sécurisation avec Spring Security
  • TP5: Documentation avec Swagger/OpenAPI
  • TP6: Déploiement en production

Ressources Supplémentaires​

Documentation​

Tutoriels​


Retour au Module 3