Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 630 minIntermédiaire

Architecture en couches : présentation, métier, données

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • décrire le rôle des couches présentation, service et accès aux données ;
  • déplacer des règles de gestion d'un contrôleur vers un service ;
  • respecter le sens des dépendances entre les couches ;
  • traduire une erreur métier en réponse HTTP ou en message à l'écran.

4 exercices corrigés1 schéma

L'architecture en couches range le code d'une application en niveaux superposés, chacun avec un rôle : la couche présentation dialogue avec l'utilisateur, la couche métier (ou couche service) applique les règles de gestion, la couche d'accès aux données lit et écrit dans la base. Chaque couche n'appelle que celle du dessous. Ce chapitre montre comment passer du MVC des chapitres précédents à cette organisation, en sortant les règles de gestion des contrôleurs pour les placer dans un service, en Node.js, en C# et en PHP.

Les trois couches classiques

Couche présentationVues : gabarits EJS, formulaires WinForms, composants ReactContrôleurs : lisent la requête, appellent le service, répondentCouche service (métier)ReservationService.reserver()durée de 4 h au plus · capacité de la sallepas de chevauchement · pas de réservation dans le passéCouche d'accès aux donnéesSalleRepository · ReservationRepositorytout le SQL, en requêtes préparéesMySQLappelleappelleSQLEntitésSalleReservationutilisées parles troiscouchesles appels ne remontent jamais
Trois couches, des appels qui ne descendent que vers le bas
CoucheRôleElle connaîtElle ignoreDans ResaSalles
PrésentationRecevoir les actions, afficher les résultatsLe service, le format d'entrée et de sortie (HTTP, formulaire)Le SQL, les règles de gestionRoutes, contrôleurs, vues
Service (métier)Appliquer les règles de gestion, orchestrer une opérationLes Repository (par leurs méthodes), les entitésHTTP, l'écran, le SQLReservationService
Accès aux donnéesLire et écrire les donnéesLa base, le SQL, le piloteLes règles, l'affichageSalleRepository, ReservationRepository

On parle souvent d'architecture 3 couches. Certaines applications en ajoutent : une couche domaine qui porte les entités riches et leurs règles, distincte d'une couche application qui orchestre les cas d'utilisation. Pour un projet de BTS, trois couches suffisent.

Couches ou tiers ?

Ne confonds pas couche (layer) et tiers (tier). Une couche est un découpage logique du code : trois dossiers dans le même projet, qui tournent dans le même programme. Un tiers est un découpage physique : des programmes qui tournent sur des machines (ou au moins des processus) différents et communiquent par le réseau. Une application Express en trois couches qui interroge MySQL, c'est trois couches, mais deux tiers côté serveur (le serveur Node.js et le serveur MySQL), plus le navigateur. Le chapitre 9 détaille les tiers.

Le contrôleur obèse

Au chapitre 3, l'action reserver du contrôleur PHP contenait les quatre règles de gestion. Ça fonctionne, mais trois demandes de la direction montrent la limite :

  1. « Les assistantes de direction doivent pouvoir réserver pour quelqu'un d'autre, depuis l'application de bureau. » Les règles sont dans le contrôleur web : il faut les recopier dans l'application de bureau.
  2. « Chaque nuit, un script importe les réservations des salles de formation depuis un fichier CSV. » Encore une copie.
  3. « La durée maximale passe de 4 à 3 heures. » Il faut retrouver et modifier toutes les copies, et en oublier une.

La solution : sortir les règles du contrôleur et les placer dans une classe qui ne dépend ni de HTTP ni de l'écran, le service. Le contrôleur web, le formulaire WinForms et le script d'import l'appellent tous.

La couche service en Node.js

js
// src/erreurs.js
// Une règle de gestion non respectée. Le service la lève, le contrôleur n'a rien à faire :
// le gestionnaire d'erreurs la traduit en réponse HTTP.
export class ErreurMetier extends Error {
  constructor(message, statut = 422) {
    super(message);
    this.name = 'ErreurMetier';
    this.statut = statut;
  }
}
js
// src/services/reservation.service.js
import { ErreurMetier } from '../erreurs.js';

const DUREE_MAX_MINUTES = 240;

// La couche service : les règles de gestion des réservations. Ni req, ni res, ni SQL.
export function creerReservationService({ salleRepository, reservationRepository, maintenant = () => new Date() }) {
  return {
    async reserver({ salleId, employeId, debut, fin, objet, participants }) {
      const salle = await salleRepository.trouverParId(salleId);
      if (!salle) throw new ErreurMetier('Salle introuvable', 404);
      if (fin <= debut) throw new ErreurMetier('La fin doit être après le début');
      if (debut < maintenant()) throw new ErreurMetier('Impossible de réserver dans le passé');
      if ((fin - debut) / 60_000 > DUREE_MAX_MINUTES) {
        throw new ErreurMetier('Une réservation dure 4 heures au plus');
      }
      if (!Number.isInteger(participants) || participants < 1) {
        throw new ErreurMetier('Indique au moins 1 participant');
      }
      if (participants > salle.capacite) {
        throw new ErreurMetier(`La salle ${salle.nom} accueille ${salle.capacite} personnes au plus`);
      }
      if (await reservationRepository.existeChevauchement(salleId, debut, fin)) {
        throw new ErreurMetier('La salle est déjà réservée sur ce créneau', 409);
      }
      return reservationRepository.ajouter({ salleId, employeId, debut, fin, objet, participants });
    },
  };
}

Le service reçoit des valeurs simples (des nombres, des dates, du texte) et renvoie la réservation créée, ou lève une ErreurMetier. Rien n'indique qu'il est appelé par une API web : on pourrait l'appeler depuis un script, un test, ou une autre interface. La constante DUREE_MAX_MINUTES est le seul endroit où figure la règle des 4 heures. Le paramètre maintenant (l'horloge) paraît étrange : il sert à tester la règle « pas dans le passé » avec une date fixe (chapitre 7).

Les Repository contiennent le SQL, comme les modèles du chapitre 4 :

js
// src/repositories/reservation.repository.js
export function creerReservationRepository(pool) {
  return {
    async existeChevauchement(salleId, debut, fin) {
      const [lignes] = await pool.execute(
        'SELECT COUNT(*) AS nb FROM reservation WHERE salle_id = ? AND debut < ? AND fin > ?',
        [salleId, fin, debut],
      );
      return lignes[0].nb > 0;
    },

    async ajouter(reservation) {
      const { salleId, employeId, debut, fin, objet, participants } = reservation;
      const [resultat] = await pool.execute(
        `INSERT INTO reservation (salle_id, employe_id, debut, fin, objet, participants)
         VALUES (?, ?, ?, ?, ?, ?)`,
        [salleId, employeId, debut, fin, objet, participants],
      );
      return { id: resultat.insertId, ...reservation };
    },
  };
}

Le contrôleur mince

js
// src/controllers/reservation.controller.js
// Le contrôleur traduit HTTP vers le métier, et le résultat vers HTTP. Il reste mince.
export function creerReservationController(reservationService) {
  return {
    async creer(req, res) {
      const { salleId, debut, fin, objet, participants } = req.body ?? {};
      const dateDebut = new Date(debut);
      const dateFin = new Date(fin);
      if (Number.isNaN(dateDebut.getTime()) || Number.isNaN(dateFin.getTime()) || !objet) {
        return res.status(400).json({ erreur: 'Requête incomplète ou dates invalides' });
      }
      const reservation = await reservationService.reserver({
        salleId: Number(salleId),
        employeId: req.utilisateur.id,
        debut: dateDebut,
        fin: dateFin,
        objet,
        participants: Number(participants),
      });
      res.status(201).json(reservation);
    },
  };
}

Le contrôleur fait trois choses : il lit la requête et vérifie qu'elle est bien formée (des dates lisibles, un objet présent), il appelle le service, il répond en HTTP. L'identité de l'employé vient de req.utilisateur, rempli par un middleware d'authentification placé avant la route (authentification) : on ne fait jamais confiance à un employeId envoyé dans le corps de la requête.

Il ne contient pas de try/catch : l'ErreurMetier levée par le service remonte jusqu'au gestionnaire d'erreurs, déclaré une fois pour toute l'application.

js
// Extrait de src/app.js : le gestionnaire d'erreurs, à la fin
app.use((err, req, res, next) => {
  if (err instanceof ErreurMetier) {
    return res.status(err.statut).json({ erreur: err.message });
  }
  console.error(err);
  res.status(500).json({ erreur: 'Erreur interne' });
});

Voici ce que répond l'API, avec une réservation existante de 9 h à 10 h 30 dans la salle Mont-Blanc (12 places) et une salle Aiguille de 4 places :

Requête (corps JSON)Réponse
Salle 1, 10 h à 11 h, 6 personnes409 {"erreur":"La salle est déjà réservée sur ce créneau"}
Salle 1, 10 h 30 à 11 h 30, 6 personnes201 et la réservation créée
Salle 2, 10 h 30 à 11 h 30, 6 personnes422 {"erreur":"La salle Aiguille accueille 4 personnes au plus"}
Salle 9404 {"erreur":"Salle introuvable"}
"debut": "demain"400 {"erreur":"Requête incomplète ou dates invalides"}
8 h à 12 h 30422 {"erreur":"Une réservation dure 4 heures au plus"}

La répartition des codes suit une logique : 400 quand la requête est mal formée (le contrôleur le voit sans rien demander à personne), 404 quand la ressource visée n'existe pas, 409 quand la demande entre en conflit avec l'état actuel des données, 422 quand elle est bien formée mais viole une règle de gestion. Le choix des codes est détaillé dans le cours API REST.

La couche service en C#

En C#, le service est une classe ; l'erreur métier, une exception.

csharp
// Services/ReservationService.cs
using ResaSalles.Data;
using ResaSalles.Models;

namespace ResaSalles.Services;

public class ErreurMetierException : Exception
{
    public ErreurMetierException(string message) : base(message) { }
}

// La couche service : les règles de gestion, et elles seules.
// Elle reçoit ses dépendances par son constructeur : c'est l'injection de dépendances.
public class ReservationService
{
    private const int DureeMaxMinutes = 240;

    private readonly ISalleRepository _salles;
    private readonly IReservationRepository _reservations;
    private readonly TimeProvider _horloge;

    public ReservationService(ISalleRepository salles, IReservationRepository reservations, TimeProvider horloge)
    {
        _salles = salles;
        _reservations = reservations;
        _horloge = horloge;
    }

    public Reservation Reserver(Reservation demande)
    {
        Salle salle = _salles.TrouverParId(demande.SalleId)
            ?? throw new ErreurMetierException("Salle introuvable.");
        if (string.IsNullOrWhiteSpace(demande.Objet))
            throw new ErreurMetierException("L'objet est obligatoire.");
        if (demande.Fin <= demande.Debut)
            throw new ErreurMetierException("La fin doit être après le début.");
        if (demande.Debut < _horloge.GetLocalNow().DateTime)
            throw new ErreurMetierException("Impossible de réserver dans le passé.");
        if ((demande.Fin - demande.Debut).TotalMinutes > DureeMaxMinutes)
            throw new ErreurMetierException("Une réservation dure 4 heures au plus.");
        if (demande.Participants < 1)
            throw new ErreurMetierException("Indique au moins 1 participant.");
        if (demande.Participants > salle.Capacite)
            throw new ErreurMetierException($"La salle {salle.Nom} accueille {salle.Capacite} personnes au plus.");
        if (_reservations.ExisteChevauchement(salle.Id, demande.Debut, demande.Fin))
            throw new ErreurMetierException("La salle est déjà réservée sur ce créneau.");

        demande.Id = _reservations.Ajouter(demande);
        return demande;
    }
}

ISalleRepository et IReservationRepository sont des interfaces que les DAO du chapitre 5, renommés SalleRepository et ReservationRepository, implémentent ; le chapitre 7 explique pourquoi. TimeProvider est la classe de .NET (depuis .NET 8) qui donne l'heure : en production, on passe TimeProvider.System ; dans un test, une horloge fixe.

Le contrôleur WinForms devient un relais, et la vue affiche le message :

csharp
// Controllers/ReservationController.cs (version en couches)
public string? Reserver(Reservation demande)
{
    try
    {
        _service.Reserver(demande);
        return null;
    }
    catch (ErreurMetierException ex)
    {
        return ex.Message;
    }
}

On attrape seulement ErreurMetierException. Une panne de la base (MySqlException) n'est pas une règle de gestion : elle ne doit pas s'afficher comme « réservation refusée » mais remonter, être journalisée et donner un message d'erreur technique.

La couche service en PHP

php
<?php
// src/Service/ReservationService.php
declare(strict_types=1);

final class ErreurMetier extends RuntimeException {}

final class ReservationService
{
    private const DUREE_MAX_SECONDES = 4 * 3600;

    public function __construct(
        private SalleModel $salles,
        private ReservationModel $reservations,
    ) {}

    public function reserver(int $salleId, int $employeId, DateTimeImmutable $debut,
                             DateTimeImmutable $fin, string $objet, int $participants): int
    {
        $salle = $this->salles->trouverParId($salleId) ?? throw new ErreurMetier('Salle introuvable.');
        if ($fin <= $debut) {
            throw new ErreurMetier('La fin doit être après le début.');
        }
        if ($debut < new DateTimeImmutable()) {
            throw new ErreurMetier('Impossible de réserver dans le passé.');
        }
        if ($fin->getTimestamp() - $debut->getTimestamp() > self::DUREE_MAX_SECONDES) {
            throw new ErreurMetier('Une réservation dure 4 heures au plus.');
        }
        if ($participants < 1) {
            throw new ErreurMetier('Indique au moins 1 participant.');
        }
        if ($participants > $salle['capacite']) {
            throw new ErreurMetier("La salle {$salle['nom']} accueille {$salle['capacite']} personnes au plus.");
        }
        $d = $debut->format('Y-m-d H:i:s');
        $f = $fin->format('Y-m-d H:i:s');
        if ($this->reservations->existeChevauchement($salleId, $d, $f)) {
            throw new ErreurMetier('La salle est déjà réservée sur ce créneau.');
        }
        return $this->reservations->ajouter($salleId, $employeId, $d, $f, $objet, $participants);
    }
}

L'action reserver du contrôleur du chapitre 3 passe d'une cinquantaine de lignes à vingt-cinq, dont aucune n'est une règle de gestion :

php
public function reserver(int $salleId): void
{
    $employeId = $_SESSION['employe_id'] ?? null;
    if ($employeId === null) {
        http_response_code(401);
        $this->afficher('erreur', ['titre' => 'Connexion requise', 'message' => 'Connecte-toi pour réserver.']);
        return;
    }
    $debut = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i', $_POST['debut'] ?? '');
    $fin = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i', $_POST['fin'] ?? '');
    $objet = trim($_POST['objet'] ?? '');
    if ($debut === false || $fin === false || $objet === '') {
        http_response_code(400);
        $this->detail($salleId, ['Remplis toutes les zones.']);
        return;
    }
    try {
        $this->service->reserver($salleId, (int) $employeId, $debut, $fin, $objet, (int) ($_POST['participants'] ?? 0));
    } catch (ErreurMetier $e) {
        http_response_code(422);
        $this->detail($salleId, [$e->getMessage()]);
        return;
    }
    header('Location: /salles/' . $salleId, true, 303);
}

Dans index.php, on crée le service et on le passe au contrôleur : new SalleController($salles, $reservations, new ReservationService($salles, $reservations)). La syntaxe ?? throw (une expression throw après l'opérateur de fusion ??) existe depuis PHP 8.0.

Les règles de dépendance entre couches

Une architecture en couches tient tant que ces règles sont respectées :

  1. Les appels descendent. La présentation appelle le service, le service appelle l'accès aux données. Jamais l'inverse : un Repository qui appelle un contrôleur, ou un service qui importe express, est une erreur.
  2. On ne saute pas de couche pour écrire. Un contrôleur qui appelle directement reservationRepository.ajouter() contourne les règles du service. Pour une simple lecture sans règle (afficher la liste des salles), certaines équipes tolèrent qu'un contrôleur lise un Repository : c'est un choix à faire une fois pour tout le projet.
  3. Les erreurs remontent sous une forme que la couche du dessus comprend : le Repository lève une erreur technique, le service une erreur métier, la présentation la traduit (code HTTP, message à l'écran).
  4. Les données qui traversent sont simples : des entités (Reservation), des objets de transfert (DTO), des types de base. Jamais un req, un TextBox ou un MySqlDataReader.

Qui vérifie quoi : la défense en profondeur

Une règle peut être vérifiée à plusieurs niveaux. Ce n'est pas du code en double, à condition que chaque niveau ait sa raison d'être :

NiveauCe qu'il vérifiePourquoi
Interface (HTML required, min, contrôles WinForms)Le confort : champs vides, nombres négatifsPrévenir l'utilisateur tout de suite. Ce contrôle se contourne : il ne protège rien.
ContrôleurLa forme de la requête : dates lisibles, types, champs présentsRejeter tôt une requête mal formée (400)
ServiceLes règles de gestion : durée, capacité, chevauchement, droitsLe seul endroit qui fait foi, quelle que soit l'interface
Base de donnéesL'intégrité : clés étrangères, NOT NULL, CHECK (fin > debut), UNIQUELe dernier rempart, même si un script contourne l'application

Un cas mérite attention : le chevauchement. Le service vérifie qu'il n'existe pas de réservation sur le créneau, puis insère. Si deux salariés réservent le même créneau à la même milliseconde, les deux vérifications passent avant que l'une des deux insertions ait eu lieu, et la salle est réservée deux fois. Pour l'éviter, on exécute la vérification et l'insertion dans une transaction qui verrouille la ligne de la salle (SELECT … FROM salle WHERE id = ? FOR UPDATE) : la deuxième demande attend la fin de la première, puis voit le conflit (transactions et verrous). Dans une application en couches, c'est le service qui ouvre et valide la transaction, puisque c'est lui qui sait quelles opérations forment un tout.

Ranger par couche ou par fonctionnalité ?

Deux façons d'organiser les dossiers coexistent :

text
Par couche (technique)              Par fonctionnalité (métier)
src/                                src/
├── controllers/                    ├── salles/
│   ├── salle.controller.js         │   ├── salle.controller.js
│   └── reservation.controller.js   │   ├── salle.service.js
├── services/                       │   └── salle.repository.js
│   └── reservation.service.js      └── reservations/
└── repositories/                       ├── reservation.controller.js
    ├── salle.repository.js             ├── reservation.service.js
    └── reservation.repository.js       └── reservation.repository.js

Le rangement par couche est le plus courant dans les projets de BTS et dans les tutoriels : on voit d'un coup d'œil l'architecture. Le rangement par fonctionnalité (feature folders) devient plus pratique quand le projet grossit : tout ce qui concerne les réservations est au même endroit. Dans les deux cas, les couches existent ; seul le rangement des fichiers change.

Exercices

Exercice 1 sur 4 Alléger un contrôleur Réussi À revoir

Voici une action d'un contrôleur Express. Réécris-la en un contrôleur mince et une méthode de service. Précise les codes HTTP.

js
async annuler(req, res) {
  const [lignes] = await pool.execute('SELECT * FROM reservation WHERE id = ?', [req.params.id]);
  const r = lignes[0];
  if (!r) return res.status(404).json({ erreur: 'Introuvable' });
  if (r.employe_id !== req.utilisateur.id) return res.status(403).json({ erreur: 'Pas ta réservation' });
  if (r.debut <= new Date()) return res.status(422).json({ erreur: 'Déjà commencée' });
  await pool.execute('DELETE FROM reservation WHERE id = ?', [r.id]);
  res.status(204).end();
}
Voir la réponseMasquer la réponse de l'exercice 1

Le service (les règles) :

js
async annuler(reservationId, employeId) {
  const r = await reservationRepository.trouverParId(reservationId);
  if (!r) throw new ErreurMetier('Réservation introuvable', 404);
  if (r.employe_id !== employeId) throw new ErreurMetier('Tu ne peux annuler que tes réservations', 403);
  if (r.debut <= maintenant()) throw new ErreurMetier('Cette réservation a déjà commencé', 422);
  await reservationRepository.supprimer(reservationId);
},

Le contrôleur (HTTP seulement) :

js
async annuler(req, res) {
  await reservationService.annuler(Number(req.params.id), req.utilisateur.id);
  res.status(204).end();
},

Le Repository reçoit deux méthodes, trouverParId et supprimer, qui contiennent les deux requêtes. Les codes ne changent pas : 404 si la réservation n'existe pas, 403 si elle appartient à quelqu'un d'autre, 422 si elle a commencé, 204 (succès sans contenu) sinon. Ils passent par l'ErreurMetier et le gestionnaire d'erreurs. Certaines équipes préfèrent répondre 404 plutôt que 403 pour ne pas révéler l'existence de la réservation d'un autre : c'est un choix de sécurité à documenter.

Tu avais trouvé ?

Exercice 2 sur 4 Quelle couche ? Réussi À revoir

Pour chaque vérification de ResaSalles, indique la couche qui doit la faire (présentation, service, accès aux données ou base) et pourquoi.

  1. Le champ « participants » contient un nombre.
  2. Un salarié ne peut pas avoir plus de 10 heures de réservations par semaine.
  3. Une réservation référence une salle qui existe.
  4. Seul un administrateur peut créer une salle.
  5. L'objet de la réservation fait 100 caractères au plus.
Voir la réponseMasquer la réponse de l'exercice 2
  1. Présentation (contrôleur) : c'est la forme de la requête. Réponse 400 si ce n'est pas un nombre.
  2. Service : c'est une règle de gestion qui demande de compter des réservations existantes (le Repository fournit la somme, le service compare).
  3. Base : une clé étrangère reservation.salle_id → salle.id garantit l'intégrité. Le service peut aussi vérifier pour donner un message clair (404).
  4. Service, ou un middleware de contrôle d'accès dans la présentation qui vérifie le rôle avant même d'appeler le service. L'important est que la vérification soit faite côté serveur, quelle que soit l'interface (contrôle d'accès).
  5. Plusieurs niveaux : maxlength="100" dans le formulaire pour le confort, une vérification dans le contrôleur ou le service pour un message clair, et VARCHAR(100) dans la base. En mode strict, MySQL refuse une valeur trop longue au lieu de la tronquer.

Tu avais trouvé ?

Exercice 3 sur 4 Repérer les violations Réussi À revoir

Pour chaque situation, dis quelle règle de dépendance entre couches est violée.

  1. reservation.repository.js contient import { request } from 'express';.
  2. Le contrôleur de suppression d'une salle appelle salleRepository.supprimer(id) sans passer par le service, qui vérifie pourtant qu'aucune réservation future n'existe.
  3. Le service renvoie un MySqlDataReader au contrôleur WinForms, qui le parcourt pour remplir la grille.
  4. ReservationService appelle MessageBox.Show("Créneau pris").
Voir la réponseMasquer la réponse de l'exercice 3
  1. La couche d'accès aux données dépend de la présentation (Express) : les appels doivent descendre, pas remonter.
  2. Le contrôleur saute la couche service pour une écriture : la règle « pas de réservation future » est contournée.
  3. Un objet technique de l'accès aux données traverse jusqu'à la présentation : le Repository doit renvoyer une List<Salle> ou une List<Reservation>, et le lecteur doit être fermé dans le Repository.
  4. Le service dépend de l'interface graphique : il ne peut plus servir à une API ni être testé. Il doit lever une exception que la vue affiche.

Tu avais trouvé ?

Exercice 4 sur 4 Deux réservations au même instant Réussi À revoir

Explique avec un scénario chronologique comment deux salariés peuvent obtenir la même salle sur le même créneau avec le service du chapitre, puis indique deux façons d'empêcher ce doublon.

Voir la réponseMasquer la réponse de l'exercice 4

Scénario : à t0, Alice demande la salle 1 de 14 h à 15 h ; le service vérifie, aucun chevauchement. À t0 + 1 ms, Bruno demande la salle 1 de 14 h 30 à 15 h 30 ; le service vérifie, aucun chevauchement non plus, car la réservation d'Alice n'est pas encore insérée. Puis les deux insertions ont lieu : la salle est réservée deux fois.

Corrections possibles :

  • Une transaction avec verrou : le service ouvre une transaction, verrouille la ligne de la salle (SELECT id FROM salle WHERE id = ? FOR UPDATE), vérifie le chevauchement, insère, valide. La demande de Bruno attend le verrou, puis voit la réservation d'Alice et est refusée.
  • Une contrainte en base quand le SGBD le permet : PostgreSQL sait exprimer « pas de chevauchement de périodes pour une même salle » (contrainte d'exclusion). MySQL ne le permet pas directement, d'où le verrou. Une contrainte UNIQUE ne suffit pas : elle n'empêche que les doublons exacts, pas les chevauchements.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Pattern Repository, DAO et injection de dépendancesRevoir le sommaire du cours