Illustration de l'article De la user story à la base de données : MCD, MLD, MPD et diagrammes UML

De la user story à la base de données : MCD, MLD, MPD et diagrammes UML

On a tous connu le projet qui démarre directement par le code. Ça avance vite la première semaine. Puis une question arrive : « un commentaire peut-il exister sans compte ? ». Personne ne l'avait posée, la base ne le permet pas et il faut tout reprendre.

La conception sert à poser ces questions avant qu'elles ne coûtent cher. Ce n'est pas de la paperasse. C'est la façon la moins chère de se tromper.

Cet article suit un seul exemple du début à la fin : un blog avec des visiteurs, des membres et un administrateur. On part du besoin exprimé en user stories et on arrive au script SQL qui crée les tables.

Une chaîne, pas une liste d'outils

Chaque étape répond à une question précise et nourrit la suivante.

Étape Question Livrable
User story Qu'est-ce que l'utilisateur veut et pourquoi ? Une phrase et des critères d'acceptation
Cas d'utilisation Qui fait quoi avec le système ? Un diagramme et des scénarios
Diagramme de séquence Comment les éléments échangent pour y arriver ? Un diagramme par scénario important
MCD Quelles données et quels liens entre elles ? Entités, associations, cardinalités
MLD Comment ranger ces données en tables ? Tables, clés primaires et étrangères
MPD Comment l'écrire pour ce SGBD précis ? Le script SQL

Les trois premières étapes viennent d'UML et des méthodes agiles. Les trois dernières viennent de Merise. On les mélange tous les jours en entreprise et c'est très bien comme ça.

La user story

Le format

Une user story décrit un besoin du point de vue de celui qui en profite.

En tant que <rôle>,
je veux <action>,
afin de <bénéfice>.

Trois exemples pour notre blog.

En tant que visiteur,
je veux rechercher un article par mot-clé,
afin de trouver rapidement un sujet qui m'intéresse.

En tant que membre,
je veux modifier mon commentaire pendant quelques minutes,
afin de corriger une faute de frappe.

En tant qu'administrateur,
je veux être prévenu par email d'un commentaire en attente,
afin de le modérer rapidement.

La troisième partie « afin de » est celle qu'on oublie le plus. C'est pourtant la plus utile. Elle explique pourquoi on fait le travail, ce qui permet de proposer une meilleure solution que celle demandée.

Les critères d'acceptation

Une story sans critères d'acceptation ne peut pas être terminée, puisque personne ne sait à quoi ressemble « fini ». La forme la plus répandue vient de Gherkin : Étant donné, Quand, Alors.

Scénario : modifier son commentaire à temps
  Étant donné un membre connecté qui a publié un commentaire il y a 5 minutes
  Quand il modifie le texte et enregistre
  Alors le nouveau texte remplace l'ancien

Scénario : délai dépassé
  Étant donné un membre connecté qui a publié un commentaire il y a 20 minutes
  Quand il affiche l'article
  Alors le bouton « Modifier » n'apparaît plus sur ce commentaire

Chaque scénario devient presque mot pour mot un test automatisé. C'est le vrai intérêt du format.

Une bonne story respecte INVEST

  • Indépendante : elle peut être livrée sans attendre une autre
  • Négociable : c'est le début d'une discussion, pas un contrat figé
  • Valeur : elle apporte quelque chose à un utilisateur
  • Estimable : l'équipe sait dire à peu près combien elle coûte
  • Small (petite) : elle tient dans une itération
  • Testable : on sait vérifier qu'elle est faite

Les pièges

La story technique déguisée.

En tant que développeur,
je veux créer la table comments,
afin de stocker les commentaires.

Aucun utilisateur ne profite d'une table. C'est une tâche, elle a sa place sous une vraie story.

La story trop grosse « En tant que membre, je veux gérer mon compte » cache une inscription, une connexion, un changement de mot de passe et une suppression. C'est une epic. Il faut la découper.

Le diagramme de cas d'utilisation

Ce qu'il montre

Il donne une vue d'ensemble : qui utilise le système et pour faire quoi. Il ne montre ni les écrans, ni l'ordre des actions, ni les données.

Ses éléments :

  • l'acteur : un rôle extérieur au système, humain ou non. « Visiteur », pas « Paul »
  • le cas d'utilisation : un objectif de l'acteur formulé avec un verbe à l'infinitif
  • le cadre : la limite du système
  • les relations : association, généralisation, inclusion et extension

L'exemple

Un bonhomme pour chaque acteur, une ellipse pour chaque cas et une flèche à triangle pour l'héritage.

Son code source, à coller tel quel sur le site de PlantUML ou dans un plugin d'IDE :

@startuml
left to right direction

actor Visiteur
actor Membre
actor Administrateur

Membre --|> Visiteur
Administrateur --|> Membre

rectangle "Blog" {
  usecase "Lire un article" as UC1
  usecase "Rechercher un article" as UC2
  usecase "Commenter un article" as UC3
  usecase "Répondre à un commentaire" as UC4
  usecase "Vérifier le contenu\n(anti-spam)" as UC5
  usecase "Modérer les commentaires" as UC6
  usecase "Publier un article" as UC7
}

Visiteur --> UC1
Visiteur --> UC2
Visiteur --> UC3
Administrateur --> UC6
Administrateur --> UC7

UC3 ..> UC5 : <<include>>
UC4 ..> UC3 : <<extend>>
@enduml

Les relations, sans contresens

La généralisation se lit « est un ». Un membre est un visiteur. Il peut donc faire tout ce que fait un visiteur, plus ses propres actions. Inutile de relier le membre à « Lire un article », c'est hérité.

L'inclusion (include) désigne un comportement toujours exécuté par le cas de base. Commenter déclenche toujours la vérification anti-spam. La flèche part du cas de base vers le cas inclus.

L'extension (extend) désigne un comportement optionnel,qui ne se produit que sous condition. Répondre à un commentaire est une variante de « commenter », utilisée seulement quand on vise un message précis. La flèche part de l'extension vers le cas de base.

L'erreur la plus fréquente consiste à découper un cas en étapes avec des include : « saisir le texte », « cliquer sur envoyer », « afficher le message ». Ce n'est plus un diagramme de cas d'utilisation, c'est un organigramme mal dessiné. Le déroulé d'un cas se décrit ailleurs.

La description textuelle

Le diagramme ne suffit pas. Chaque cas important s'accompagne d'une fiche.

Cas : Commenter un article
Acteur principal : Visiteur
Précondition : l'article est publié

Scénario nominal
  1. Le visiteur saisit son nom, son email et son commentaire
  2. Le système vérifie le contenu (anti-spam)
  3. Le système enregistre le commentaire en attente de modération
  4. Le système prévient l'administrateur par email
  5. Le système affiche un message de confirmation

Scénarios alternatifs
  2a. Le contenu est refusé : le système affiche le formulaire avec l'erreur
  3a. L'acteur est un membre connecté : le commentaire est publié directement

Postcondition : le commentaire existe en base

Le diagramme de séquence

Ce qu'il montre

Il prend un scénario et montre qui envoie quel message à qui, dans quel ordre. Le temps s'écoule de haut en bas.

Ses éléments :

  • la ligne de vie : un participant, avec un trait vertical en dessous
  • le message synchrone (flèche pleine, ->> en Mermaid) : l'émetteur attend la réponse
  • le message asynchrone (flèche ouverte, -)) : l'émetteur n'attend pas. Un email mis en file d'attente par exemple
  • le retour (flèche en pointillés, -->>) : la réponse à un message
  • la barre d'activation : la période où le participant travaille. En Mermaid, un + ou un - collé à la flèche l'ouvre ou la ferme
  • les fragments : alt pour une alternative, opt pour une option, loop pour une boucle

L'exemple

Mermaid, qui dessine les autres diagrammes de cet article ne connaît pas le diagramme de cas d'utilisation. Celui-ci est donc dessiné avec PlantUML, un outil qui respecte la notation UML : un bonhomme pour chaque acteur, une ellipse pour chaque cas et une flèche à triangle pour l'héritage.

sequenceDiagram
    actor Membre
    participant Nav as Navigateur
    participant Router as Routeur Laravel
    participant Req as StoreCommentRequest
    participant Ctrl as BlogController
    participant Model as Comment
    participant DB as Base de données
    participant Mail as Service de mail

    Membre->>Nav: envoie le formulaire
    Nav->>+Router: POST /blog/{slug}/comment
    Router->>+Req: valider les données

    alt données invalides
        Req-->>Nav: 302 vers le formulaire + erreurs
    else données valides
        Req-->>-Router: données validées
        Router->>+Ctrl: addComment(request, slug)
        Ctrl->>Model: create(données)
        Model->>DB: INSERT INTO comments
        DB-->>Model: id
        Model-->>Ctrl: Comment

        opt réponse à un commentaire
            Ctrl->>Mail: prévenir l'auteur visé
        end

        Ctrl-->>-Nav: 302 vers l'article + message
    end

    deactivate Router

Un détail montre l'intérêt de l'exercice. En Laravel, la validation d'une FormRequest a lieu avant l'appel du contrôleur. Quand les données sont invalides, le contrôleur n'est jamais exécuté. Dessiner la séquence oblige à le savoir.

Le bon niveau de détail

Un diagramme de séquence n'est pas du code dessiné. On n'y met ni les getters, ni chaque appel de fonction. On garde les échanges qui comptent pour comprendre le scénario et on en fait un par scénario important, pas un par méthode.

Merise : du MCD au MPD

Merise est une méthode française, née à la fin des années 1970. Elle sépare la conception d'une base de données en trois niveaux.

  • Conceptuel (MCD) : quelles données, sans penser à la technique
  • Logique (MLD) : comment les ranger dans des tables
  • Physique (MPD) : comment les écrire pour un SGBD précis

Chaque niveau se déduit du précédent avec des règles simples. C'est ce qui rend la méthode si pratique.

Le dictionnaire de données

Avant de dessiner, on liste les informations à stocker avec leur type et leurs contraintes.

Donnée Type Contrainte
pseudo texte, 30 caractères obligatoire, unique
email texte, 255 caractères obligatoire, unique
titre texte obligatoire
contenu du commentaire texte long obligatoire
date de publication date et heure facultative
libellé du tag texte, 50 caractères obligatoire, unique

On en profite pour chasser les données calculées. Le nombre de commentaires d'un article ne se stocke pas : il se compte.

Le MCD

Le MCD contient trois sortes d'éléments.

  • L'entité : un objet du domaine avec ses propriétés. UTILISATEUR, ARTICLE
  • L'identifiant : la propriété qui distingue chaque occurrence. id_article
  • L'association : un lien entre entités nommé par un verbe. ÉCRIRE, CONCERNER

Notre MCD, sous forme textuelle :

UTILISATEUR (id_utilisateur, pseudo, email, mot_de_passe)
ARTICLE     (id_article, titre, contenu, date_publication)
COMMENTAIRE (id_commentaire, contenu, date_creation)
TAG         (id_tag, libelle)

UTILISATEUR 0,n ---- ÉCRIRE ---- 0,1 COMMENTAIRE
ARTICLE     0,n ---- CONCERNER - 1,1 COMMENTAIRE
ARTICLE     0,n ---- ÉTIQUETER - 0,n TAG
UTILISATEUR 0,n ---- AIMER ----- 0,n ARTICLE
                     (date_aime)

Lire les cardinalités

Les cardinalités se placent à côté de chaque entité. Elles disent combien de fois une occurrence de cette entité participe à l'association, au minimum puis au maximum.

Cardinalité Sens
0,1 au plus une fois, peut-être jamais
1,1 exactement une fois
0,n autant de fois qu'on veut, peut-être jamais
1,n au moins une fois

On lit toujours depuis l'entité vers l'association :

  • un utilisateur écrit 0 à n commentaires
  • un commentaire est écrit par 0 ou 1 utilisateur

Ce 0,1 n'est pas une erreur. Sur notre blog, un visiteur peut commenter sans compte. Le commentaire n'a alors aucun utilisateur. C'est exactement la question du début de l'article et le MCD l'a posée avant qu'une seule table existe.

À l'inverse, un commentaire concerne exactement un article (1,1). Un commentaire sans article n'a pas de sens.

Attention si tu viens d'UML : dans un diagramme de classes, les multiplicités se placent de l'autre côté de l'association. Le sens est le même, seule la position change.

L'association porteuse de données

AIMER porte une propriété : la date à laquelle le membre a aimé l'article. Cette date n'appartient ni à l'utilisateur, ni à l'article. Elle appartient au couple des deux. C'est le signe qu'elle doit vivre dans l'association.

Le MLD : trois règles de passage

Règle 1. Chaque entité devient une table. Son identifiant devient la clé primaire.

Règle 2. Une association avec un côté x,1 et un côté x,n disparaît. L'identifiant de l'entité côté x,n part comme clé étrangère dans la table côté x,1.

  • côté 1,1 : la clé étrangère est obligatoire (NOT NULL)
  • côté 0,1 : la clé étrangère est facultative (NULL autorisé)

Règle 3. Une association x,n des deux côtés devient une table. Sa clé primaire est composée des deux clés étrangères. Les propriétés de l'association y vont aussi.

Le résultat, en notation textuelle. La clé primaire est en gras, les clés étrangères commencent par #.

  • UTILISATEUR (id_utilisateur, pseudo, email, mot_de_passe)
  • ARTICLE (id_article, titre, contenu, date_publication)
  • TAG (id_tag, libelle)
  • COMMENTAIRE (id_commentaire, contenu, date_creation, #id_utilisateur, #id_article)
  • ÉTIQUETER (#id_article, #id_tag)
  • AIMER (#id_utilisateur, #id_article, date_aime)

ÉCRIRE et CONCERNER ont disparu : elles sont devenues les clés étrangères de COMMENTAIRE (règle 2). ÉTIQUETER et AIMER sont devenues des tables (règle 3).

Un dernier cas mérite d'être connu : l'association 0,1 / 1,1, une relation un à un. La clé étrangère va du côté 1,1, avec une contrainte d'unicité pour garantir qu'elle n'apparaît qu'une fois.

Les formes normales

Un MLD bien construit respecte au moins les trois premières formes normales.

  • 1FN : chaque colonne contient une seule valeur. Pas de colonne tags qui vaut « php, laravel, sql »
  • 2FN : en 1FN et chaque colonne dépend de toute la clé primaire. Dans AIMER, une colonne pseudo ne dépendrait que de id_utilisateur : elle n'a rien à faire là
  • 3FN : en 2FN et aucune colonne ne dépend d'une autre colonne non clé. Stocker le code postal et la ville dans la même table recopie la ville à chaque fois

Un MCD correct donne en général un MLD déjà normalisé. Les formes normales servent surtout à vérifier.

Le MPD

Le MPD traduit le MLD pour un SGBD précis. On y prend des décisions qui n'existaient pas avant : types exacts, longueurs, index, comportement à la suppression, conventions de nommage.

Ici, MySQL avec les conventions de Laravel : noms en anglais, tables au pluriel, en snake_case. UTILISATEUR devient users, pseudo devient username, ÉTIQUETER devient article_tag. Renommer fait partie des décisions physiques, le modèle ne change pas.

CREATE TABLE users (
    id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username   VARCHAR(30)  NOT NULL UNIQUE,
    email      VARCHAR(255) NOT NULL UNIQUE,
    password   VARCHAR(255) NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

CREATE TABLE articles (
    id           BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    title        VARCHAR(255) NOT NULL,
    content      LONGTEXT     NOT NULL,
    published_at TIMESTAMP    NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

CREATE TABLE tags (
    id    BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    label VARCHAR(50) NOT NULL UNIQUE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

CREATE TABLE comments (
    id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    content    TEXT            NOT NULL,
    created_at TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    user_id    BIGINT UNSIGNED NULL,
    article_id BIGINT UNSIGNED NOT NULL,
    CONSTRAINT fk_comments_user
        FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL,
    CONSTRAINT fk_comments_article
        FOREIGN KEY (article_id) REFERENCES articles (id) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

CREATE TABLE article_tag (
    article_id BIGINT UNSIGNED NOT NULL,
    tag_id     BIGINT UNSIGNED NOT NULL,
    PRIMARY KEY (article_id, tag_id),
    FOREIGN KEY (article_id) REFERENCES articles (id) ON DELETE CASCADE,
    FOREIGN KEY (tag_id)     REFERENCES tags (id)     ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

CREATE TABLE likes (
    user_id    BIGINT UNSIGNED NOT NULL,
    article_id BIGINT UNSIGNED NOT NULL,
    liked_at   TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (user_id, article_id),
    FOREIGN KEY (user_id)    REFERENCES users (id)    ON DELETE CASCADE,
    FOREIGN KEY (article_id) REFERENCES articles (id) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

Le même schéma, dessiné par Mermaid :

erDiagram
    users |o--o{ comments : "écrit"
    articles ||--o{ comments : "concerne"
    articles ||--o{ article_tag : "porte"
    tags ||--o{ article_tag : "étiquette"
    users ||--o{ likes : "aime"
    articles ||--o{ likes : "est aimé"

    users {
        bigint id PK
        varchar username UK
        varchar email UK
        varchar password
    }
    articles {
        bigint id PK
        varchar title
        longtext content
        timestamp published_at
    }
    comments {
        bigint id PK
        text content
        timestamp created_at
        bigint user_id FK "NULL autorisé"
        bigint article_id FK
    }
    tags {
        bigint id PK
        varchar label UK
    }
    article_tag {
        bigint article_id PK, FK
        bigint tag_id PK, FK
    }
    likes {
        bigint user_id PK, FK
        bigint article_id PK, FK
        timestamp liked_at
    }

Ce diagramme utilise la notation dite « patte de corbeau ». || veut dire exactement un, |o zéro ou un, o{ zéro ou plusieurs. Ce sont les cardinalités du MCD, placées de l'autre côté de la ligne : le |o près de users se lit « un commentaire a zéro ou un auteur ».

Chaque choix se justifie par le MCD :

  • user_id accepte NULL parce que la cardinalité était 0,1
  • ON DELETE SET NULL garde les commentaires d'un compte supprimé, sans auteur
  • ON DELETE CASCADE sur article_id supprime les commentaires avec leur article puisqu'un commentaire n'existe pas sans article
  • la clé primaire composée de likes empêche d'aimer deux fois le même article
  • utf8mb4 accepte tous les caractères, y compris les emojis que les visiteurs ne manqueront pas d'écrire

Avec Laravel, le MPD s'écrit sous forme de migrations plutôt qu'en SQL brut. Le raisonnement reste exactement le même.

Et le diagramme de classes ?

Hors de France, on modélise plutôt les données avec un diagramme de classes UML ou un diagramme entité-association. Les deux approches se ressemblent, mais elles ne servent pas à la même chose.

  • le MCD décrit des données et leurs liens. Rien d'autre
  • le diagramme de classes décrit des objets : leurs données, mais aussi leurs méthodes, l'héritage et les interfaces

Avec un ORM, les cardinalités se retrouvent directement dans le code.

Cardinalités du MCD Eloquent JPA
x,n / x,1 hasMany et belongsTo @OneToMany et @ManyToOne
x,1 / 0,1 hasOne et belongsTo @OneToOne
x,n / x,n belongsToMany @ManyToMany
x,n / x,n avec propriétés belongsToMany et withPivot() une entité dédiée avec deux @ManyToOne

La dernière ligne est importante. Dès qu'une association porte des données, comme la date de AIMER, JPA demande une vraie entité pour la table d'association. Un @ManyToMany ne sait pas stocker de colonne en plus.

Ce qu'il faut garder en tête

La conception est un outil de discussion. Un diagramme que personne ne lit ne sert à rien. Un diagramme qui fait dire à quelqu'un « ah non, un commentaire peut exister sans compte » vient de faire gagner une semaine.

On ne fait pas tous les diagrammes pour toutes les fonctionnalités. Un formulaire de contact n'a pas besoin d'un diagramme de séquence. Une fonctionnalité qui touche au paiement, oui.

Un modèle vit avec le projet. Quand la base change, le MCD change aussi. Sinon, il ment et un document qui ment est pire que pas de document du tout.

Pour aller plus loin

  • Mermaid, la syntaxe des diagrammes de cet article, que GitHub et GitLab affichent aussi directement
  • Mermaid Live Editor, pour tester un diagramme et voir le rendu en direct
  • PlantUML, pour la notation UML complète, y compris les vrais diagrammes de cas d'utilisation
  • Looping, un logiciel gratuit pour dessiner MCD et MLD selon Merise
  • La spécification UML de l'OMG, la référence officielle, à consulter plutôt qu'à lire
  • User Stories Applied de Mike Cohn, le livre de référence sur les user stories
  • UML Distilled de Martin Fowler, court et pragmatique

Un exercice pour finir. Prends une fonctionnalité de ton projet actuel et écris sa user story avec deux critères d'acceptation. Dessine ensuite la partie du MCD qu'elle touche. Si une cardinalité te fait hésiter, c'est une question à poser à quelqu'un avant de coder.

Commentaires (0)

Laisser un commentaire

Vous n'êtes pas connecté

Vous pouvez commenter en renseignant votre nom et votre email. Votre message sera publié après modération. Avec un compte, il apparaît immédiatement et reste modifiable.

Votre email ne sera pas publié.

Aucun commentaire pour le moment

Soyez le premier à commenter cet article !