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
- Architecture en Couches
- Création du Projet
- Création de l'Entité Student
- Création du Repository
- Création du Service
- Création du Controller
- Configuration H2
- Tests des Endpoints
- 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ètre | Valeur |
|---|---|
| Project | Maven |
| Language | Java |
| Spring Boot | 3.2.0 (ou dernière version stable) |
| Group | com.example |
| Artifact | student-management |
| Name | student-management |
| Package name | com.example.student |
| Packaging | Jar |
| Java | 17 |
Dépendances à ajouter:
- Spring Web - Pour créer l'API REST
- Spring Data JPA - Pour la persistance
- H2 Database - Base de données en mémoire
- Validation - Pour valider les données
- Spring Boot DevTools - Pour le rechargement automatique
2.2 Import dans VS Code
- Téléchargez le projet (ZIP)
- Décompressez dans votre workspace
- Ouvrez VS Code
- File → Open Folder → Sélectionnez
student-management - 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
| Annotation | Rôle |
|---|---|
@Entity | Marque la classe comme entité JPA |
@Table(name = "students") | Nom de la table SQL |
@Id | Clé primaire |
@GeneratedValue(strategy = GenerationType.IDENTITY) | Auto-incrémentation |
@Column | Configuration de la colonne |
@NotBlank | Validation: champ non vide |
@Size | Validation: taille min/max |
@Email | Validation: 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éthode | Description |
|---|---|
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 emailexistsByEmail(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:
- Validation: Vérifier si l'email existe avant création
- Gestion d'erreurs: Lancer des exceptions si étudiant non trouvé
- Orchestration: Coordonner les appels au Repository
- 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 HTTP | Endpoint | Action | Code Succès |
|---|---|---|---|
| POST | /api/students | Créer un étudiant | 201 CREATED |
| GET | /api/students | Lister tous les étudiants | 200 OK |
| GET | /api/students/\{id\} | Récupérer un étudiant | 200 OK |
| PUT | /api/students/\{id\} | Mettre à jour un étudiant | 200 OK |
| DELETE | /api/students/\{id\} | Supprimer un étudiant | 204 NO CONTENT |
| GET | /api/students/email/\{email\} | Chercher par email | 200 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:
| Champ | Valeur |
|---|---|
| JDBC URL | jdbc:h2:mem:studentdb |
| User Name | sa |
| 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
- Téléchargez Postman: https://www.postman.com/downloads/
- 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
- Accédez à
http://localhost:8080/h2-console - Connectez-vous
- 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:
- Modifiez la classe
Student.java - Ajoutez le champ
program - Générez les getters/setters
- 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:
- Ajoutez une méthode dans
StudentRepository:List<Student> findByLastName(String lastName); - Ajoutez une méthode dans
StudentService - Ajoutez un endpoint dans
StudentController:@GetMapping("/lastname/\{lastName\}") - Testez avec Postman
Exercice 3: Pagination
Objectif: Ajouter la pagination à la liste des étudiants
Instructions:
- 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);
} - Modifiez le Controller pour accepter les paramètres
pageetsize - 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:
- Créez
ResourceNotFoundException.java:
package com.example.student.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
- 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);
}
}
- Utilisez
ResourceNotFoundExceptiondans le Service - Testez en essayant d'accéder à un étudiant inexistant
Exercice 5: Validation Avancée
Objectif: Ajouter des validations personnalisées
Instructions:
- 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; - Testez en créant un étudiant avec un âge invalide
- Observez le code 400 BAD REQUEST
Exercice 6: Migration vers PostgreSQL
Objectif: Remplacer H2 par PostgreSQL
Instructions:
- Ajoutez la dépendance PostgreSQL dans
pom.xml - 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 - Créez la base de données
studentdbdans PostgreSQL - Relancez l'application
- 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
-
Quelle est la responsabilité de chaque couche (Controller, Service, Repository)?
-
Pourquoi séparer le code en couches?
-
Quelle est la différence entre
@RestControlleret@Service? -
Que fait l'annotation
@Validdans le Controller? -
Comment Spring Data JPA génère-t-il les requêtes SQL?
-
Quelle est la différence entre
save()pour créer et pour mettre à jour? -
Pourquoi utiliser
Optional<Student>dans le Service? -
Quel code HTTP retourner après une suppression réussie?
Livrables
À remettre:
- Code source complet du projet
- Captures d'écran Postman montrant les 6 tests réussis
- Capture d'écran H2 Console avec des données
- Au moins 2 exercices complétés
- Document de réponses aux questions de révision
Critères d'Évaluation
| Critère | Points |
|---|---|
| Architecture en couches correctement implémentée | 20% |
| Entité Student avec validations | 15% |
| Repository fonctionnel | 10% |
| Service avec logique métier | 20% |
| Controller avec tous les endpoints CRUD | 20% |
| Tests réussis (Postman/Browser) | 10% |
| Exercices supplémentaires | 5% |
| Total | 100% |
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