Illustrazione dell'articolo Dalla user story al database: modello concettuale, logico e fisico e diagrammi UML

Dalla user story al database: modello concettuale, logico e fisico e diagrammi UML

Tutti abbiamo visto il progetto che parte direttamente dal codice. La prima settimana si va veloci. Poi arriva una domanda: «un commento può esistere senza un account?». Nessuno l'aveva posta, il database non lo permette e bisogna rifare tutto.

La progettazione serve a porre queste domande prima che costino care. Non è burocrazia. È il modo più economico di sbagliare.

Questo articolo segue un solo esempio dall'inizio alla fine: un blog con visitatori, membri e un amministratore. Si parte dal bisogno, scritto come user story, e si arriva allo script SQL che crea le tabelle.

Una catena, non un elenco di strumenti

Ogni passaggio risponde a una domanda precisa e alimenta il successivo.

Passaggio Domanda Risultato
User story Cosa vuole l'utente, e perché? Una frase e dei criteri di accettazione
Caso d'uso Chi fa cosa con il sistema? Un diagramma e degli scenari
Diagramma di sequenza Come comunicano le parti per arrivarci? Un diagramma per ogni scenario importante
Modello concettuale Quali dati e quali legami tra loro? Entità, associazioni, cardinalità
Modello logico Come organizzare questi dati in tabelle? Tabelle, chiavi primarie ed esterne
Modello fisico Come scriverlo per questo DBMS preciso? Lo script SQL

I primi tre passaggi vengono da UML e dai metodi agili. Gli ultimi tre seguono la progettazione concettuale, logica e fisica che in Italia si studia con il modello E-R (entità-relazione). In Francia lo stesso approccio si chiama Merise. In azienda questi strumenti si mescolano ogni giorno, ed è giusto così.

La user story

Il formato

Una user story descrive un bisogno dal punto di vista di chi ne trae vantaggio.

Come <ruolo>,
voglio <azione>,
così da <beneficio>.

Tre esempi per il nostro blog.

Come visitatore,
voglio cercare un articolo per parola chiave,
così da trovare in fretta un argomento che mi interessa.

Come membro,
voglio modificare il mio commento per qualche minuto,
così da correggere un errore di battitura.

Come amministratore,
voglio essere avvisato via email di un commento in attesa,
così da moderarlo rapidamente.

La terza parte, «così da», è quella che si dimentica più spesso. Eppure è la più utile. Spiega perché si fa il lavoro, e permette di proporre una soluzione migliore di quella richiesta.

I criteri di accettazione

Una story senza criteri di accettazione non può mai essere finita, perché nessuno sa che aspetto ha «fatto». La forma più diffusa viene da Gherkin: Dato, Quando, Allora.

Scenario: modificare il commento in tempo
  Dato un membro connesso che ha pubblicato un commento 5 minuti fa
  Quando modifica il testo e salva
  Allora il nuovo testo sostituisce quello vecchio

Scenario: troppo tardi
  Dato un membro connesso che ha pubblicato un commento 20 minuti fa
  Quando apre l'articolo
  Allora il pulsante «Modifica» non compare più su quel commento

Ogni scenario diventa un test automatico quasi parola per parola. È il vero valore del formato.

Una buona story rispetta INVEST

  • Independent (indipendente): può essere consegnata senza aspettarne un'altra
  • Negotiable (negoziabile): è l'inizio di una discussione, non un contratto rigido
  • Valuable (di valore): porta qualcosa a un utente
  • Estimable (stimabile): il team sa dire più o meno quanto costa
  • Small (piccola): sta in un'iterazione
  • Testable (verificabile): si sa come controllare che sia fatta

Le trappole

La story tecnica travestita.

Come sviluppatore,
voglio creare la tabella comments,
così da salvare i commenti.

Nessun utente trae vantaggio da una tabella. È un'attività tecnica, e va messa sotto una vera story.

La story troppo grande. «Come membro, voglio gestire il mio account» nasconde una registrazione, un accesso, un cambio di password e una cancellazione. È una epic. Va divisa.

Il diagramma dei casi d'uso

Cosa mostra

Dà una visione d'insieme: chi usa il sistema, e per fare cosa. Non mostra né le schermate, né l'ordine delle azioni, né i dati.

I suoi elementi:

  • l'attore: un ruolo esterno al sistema, umano o no. «Visitatore», non «Paolo»
  • il caso d'uso: un obiettivo dell'attore, espresso con un verbo
  • il confine: il limite del sistema
  • le relazioni: associazione, generalizzazione, inclusione ed estensione

L'esempio

Mermaid, che disegna gli altri diagrammi di questo articolo, non gestisce i diagrammi dei casi d'uso. Questo è quindi disegnato con PlantUML, uno strumento che rispetta la notazione UML: un omino per ogni attore, un'ellisse per ogni caso d'uso e una freccia con triangolo vuoto per l'ereditarietà.

Diagramma dei casi d'uso del blog

Il suo codice sorgente, da incollare così com'è sul sito di PlantUML o in un plugin dell'IDE:

@startuml
left to right direction

actor Visitatore
actor Membro
actor Amministratore

Membro --|> Visitatore
Amministratore --|> Membro

rectangle "Blog" {
  usecase "Leggere un articolo" as UC1
  usecase "Cercare un articolo" as UC2
  usecase "Commentare un articolo" as UC3
  usecase "Rispondere a un commento" as UC4
  usecase "Verificare il contenuto\n(anti-spam)" as UC5
  usecase "Moderare i commenti" as UC6
  usecase "Pubblicare un articolo" as UC7
}

Visitatore --> UC1
Visitatore --> UC2
Visitatore --> UC3
Amministratore --> UC6
Amministratore --> UC7

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

Le relazioni, senza fraintendimenti

La generalizzazione si legge «è un». Un membro è un visitatore. Può quindi fare tutto ciò che fa un visitatore, più le proprie azioni. Inutile collegare il membro a «Leggere un articolo»: è ereditato.

L'inclusione (include) indica un comportamento eseguito sempre dal caso base. Commentare avvia sempre il controllo anti-spam. La freccia va dal caso base verso il caso incluso.

L'estensione (extend) indica un comportamento opzionale, che avviene solo a una certa condizione. Rispondere a un commento è una variante di «commentare», usata solo quando si risponde a un messaggio preciso. La freccia va dall'estensione verso il caso base.

L'errore più frequente è spezzare un caso d'uso in passi con degli include: «scrivere il testo», «cliccare su invia», «mostrare il messaggio». Non è più un diagramma dei casi d'uso, è un diagramma di flusso disegnato male. Lo svolgimento di un caso si descrive altrove.

La descrizione testuale

Il diagramma non basta. Ogni caso importante è accompagnato da una scheda.

Caso d'uso: Commentare un articolo
Attore principale: Visitatore
Precondizione: l'articolo è pubblicato

Scenario principale
  1. Il visitatore inserisce nome, email e commento
  2. Il sistema verifica il contenuto (anti-spam)
  3. Il sistema salva il commento in attesa di moderazione
  4. Il sistema avvisa l'amministratore via email
  5. Il sistema mostra un messaggio di conferma

Scenari alternativi
  2a. Il contenuto viene rifiutato: il sistema mostra il modulo con l'errore
  3a. L'attore è un membro connesso: il commento viene pubblicato subito

Postcondizione: il commento esiste nel database

Il diagramma di sequenza

Cosa mostra

Prende uno scenario e mostra chi invia quale messaggio a chi, in quale ordine. Il tempo scorre dall'alto verso il basso.

I suoi elementi:

  • la linea di vita: un partecipante, con una linea verticale sotto di sé
  • il messaggio sincrono (freccia piena, ->> in Mermaid): il mittente aspetta la risposta
  • il messaggio asincrono (freccia aperta, -)): il mittente non aspetta. Un'email messa in coda, per esempio
  • il ritorno (freccia tratteggiata, -->>): la risposta a un messaggio
  • la barra di attivazione: il periodo in cui il partecipante lavora. In Mermaid, un + o un - subito dopo la freccia la apre o la chiude
  • i frammenti: alt per un'alternativa, opt per un'opzione, loop per un ciclo

L'esempio

Lo scenario «un membro commenta un articolo», così come si svolge davvero in un'applicazione Laravel.

sequenceDiagram
    actor Membro
    participant Nav as Browser
    participant Router as Router Laravel
    participant Req as StoreCommentRequest
    participant Ctrl as BlogController
    participant Model as Comment
    participant DB as Database
    participant Mail as Servizio email

    Membro->>Nav: invia il modulo
    Nav->>+Router: POST /blog/{slug}/comment
    Router->>+Req: validare i dati

    alt dati non validi
        Req-->>Nav: 302 verso il modulo + errori
    else dati validi
        Req-->>-Router: dati validati
        Router->>+Ctrl: addComment(request, slug)
        Ctrl->>Model: create(dati)
        Model->>DB: INSERT INTO comments
        DB-->>Model: id
        Model-->>Ctrl: Comment

        opt risposta a un commento
            Ctrl->>Mail: avvisare l'autore interessato
        end

        Ctrl-->>-Nav: 302 verso l'articolo + messaggio
    end

    deactivate Router

Un dettaglio mostra perché l'esercizio vale la pena. In Laravel, una FormRequest viene validata prima che il controller sia chiamato. Quando i dati non sono validi, il controller non viene mai eseguito. Disegnare la sequenza obbliga a saperlo.

Il giusto livello di dettaglio

Un diagramma di sequenza non è codice disegnato. Non ci si mettono i getter né ogni chiamata di funzione. Si tengono gli scambi che servono a capire lo scenario, e se ne fa uno per ogni scenario importante, non uno per metodo.

I tre livelli della modellazione dei dati

La progettazione di un database si divide in tre livelli.

  • Concettuale: quali dati, senza pensare alla tecnologia
  • Logico: come organizzarli in tabelle
  • Fisico: come scriverli per un DBMS preciso

Ogni livello si ricava dal precedente con regole semplici. È questo che rende l'approccio così pratico.

Il dizionario dei dati

Prima di disegnare, si elencano le informazioni da salvare, con il loro tipo e i loro vincoli.

Dato Tipo Vincolo
username testo, 30 caratteri obbligatorio, unico
email testo, 255 caratteri obbligatorio, unico
titolo testo obbligatorio
contenuto del commento testo lungo obbligatorio
data di pubblicazione data e ora facoltativo
etichetta del tag testo, 50 caratteri obbligatorio, unico

È anche il momento di eliminare i dati calcolati. Il numero di commenti di un articolo non si salva: si conta.

Il modello concettuale

Il modello concettuale contiene tre tipi di elementi.

  • L'entità: un oggetto del dominio, con le sue proprietà. UTENTE, ARTICOLO
  • L'identificatore: la proprietà che distingue ogni occorrenza. id_articolo
  • L'associazione: un legame tra entità, con il nome di un verbo. SCRIVE, RIGUARDA

Il nostro modello concettuale, in forma testuale:

UTENTE    (id_utente, username, email, password)
ARTICOLO  (id_articolo, titolo, contenuto, data_pubblicazione)
COMMENTO  (id_commento, contenuto, data_creazione)
TAG       (id_tag, etichetta)

UTENTE    0,n ---- SCRIVE ---- 0,1 COMMENTO
ARTICOLO  0,n ---- RIGUARDA -- 1,1 COMMENTO
ARTICOLO  0,n ---- ETICHETTA - 0,n TAG
UTENTE    0,n ---- APPREZZA -- 0,n ARTICOLO
                   (data_apprezzamento)

Leggere le cardinalità

In questa notazione, le cardinalità stanno accanto a ogni entità. Dicono quante volte un'occorrenza di questa entità partecipa all'associazione, prima il minimo, poi il massimo.

Cardinalità Significato
0,1 al massimo una volta, forse mai
1,1 esattamente una volta
0,n quante volte si vuole, forse mai
1,n almeno una volta

Si legge sempre dall'entità verso l'associazione:

  • un utente scrive da 0 a n commenti
  • un commento è scritto da 0 o 1 utente

Quello 0,1 non è un errore. Sul nostro blog, un visitatore può commentare senza account. Il commento allora non ha nessun utente. È esattamente la domanda dell'inizio dell'articolo, e il modello concettuale l'ha posta prima che esistesse una sola tabella.

Al contrario, un commento riguarda esattamente un articolo (1,1). Un commento senza articolo non ha senso.

Attenzione se vieni da UML: in un diagramma delle classi, le molteplicità stanno dall'altra parte dell'associazione. Il significato è lo stesso, cambia solo la posizione.

L'associazione che porta dati

APPREZZA porta una proprietà: la data in cui il membro ha apprezzato l'articolo. Questa data non appartiene né all'utente né all'articolo. Appartiene alla coppia. È il segno che deve stare nell'associazione.

Il modello logico: tre regole di traduzione

Regola 1. Ogni entità diventa una tabella. Il suo identificatore diventa la chiave primaria.

Regola 2. Un'associazione con un lato x,1 e un lato x,n scompare. L'identificatore dell'entità sul lato x,n diventa una chiave esterna nella tabella sul lato x,1.

  • lato 1,1: la chiave esterna è obbligatoria (NOT NULL)
  • lato 0,1: la chiave esterna è facoltativa (NULL ammesso)

Regola 3. Un'associazione x,n da entrambi i lati diventa una tabella. La sua chiave primaria è composta dalle due chiavi esterne. Anche le proprietà dell'associazione vanno lì.

Il risultato, in notazione testuale. La chiave primaria è in grassetto, le chiavi esterne iniziano con #.

  • UTENTE (id_utente, username, email, password)
  • ARTICOLO (id_articolo, titolo, contenuto, data_pubblicazione)
  • TAG (id_tag, etichetta)
  • COMMENTO (id_commento, contenuto, data_creazione, #id_utente, #id_articolo)
  • ETICHETTA (#id_articolo, #id_tag)
  • APPREZZA (#id_utente, #id_articolo, data_apprezzamento)

SCRIVE e RIGUARDA sono scomparse: sono diventate le chiavi esterne di COMMENTO (regola 2). ETICHETTA e APPREZZA sono diventate tabelle (regola 3).

Un ultimo caso merita di essere conosciuto: l'associazione 0,1 / 1,1, un legame uno a uno. La chiave esterna va sul lato 1,1, con un vincolo di unicità per garantire che compaia una sola volta.

Le forme normali

Un modello logico ben costruito rispetta almeno le prime tre forme normali.

  • 1FN: ogni colonna contiene un solo valore. Niente colonna tags che vale «php, laravel, sql»
  • 2FN: in 1FN, e ogni colonna dipende da tutta la chiave primaria. In APPREZZA, una colonna username dipenderebbe solo da id_utente: non c'entra nulla
  • 3FN: in 2FN, e nessuna colonna dipende da un'altra colonna non chiave. Salvare il CAP e la città nella stessa tabella ricopia la città ogni volta

Un modello concettuale corretto dà in genere un modello logico già normalizzato. Le forme normali servono soprattutto a verificare.

Il modello fisico

Il modello fisico traduce il modello logico per un DBMS preciso. Porta decisioni che prima non esistevano: tipi esatti, lunghezze, indici, comportamento alla cancellazione, convenzioni di nomenclatura.

Qui, MySQL con le convenzioni di Laravel: nomi in inglese, tabelle al plurale, in snake_case. UTENTE diventa users, ETICHETTA diventa article_tag. Rinominare fa parte delle decisioni fisiche, il modello non cambia.

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;

Lo stesso schema, disegnato da Mermaid:

erDiagram
    users |o--o{ comments : "scrive"
    articles ||--o{ comments : "riguarda"
    articles ||--o{ article_tag : "ha"
    tags ||--o{ article_tag : "etichetta"
    users ||--o{ likes : "apprezza"
    articles ||--o{ likes : "è apprezzato"

    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 ammesso"
        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
    }

Questo diagramma usa la notazione detta «a zampa di gallina» (crow's foot). || significa esattamente uno, |o zero o uno, o{ zero o molti. Sono le cardinalità del modello concettuale, messe dall'altra parte della linea: lo |o vicino a users si legge «un commento ha zero o un autore».

Ogni scelta si giustifica con il modello concettuale:

  • user_id accetta NULL perché la cardinalità era 0,1
  • ON DELETE SET NULL conserva i commenti di un account cancellato, senza autore
  • ON DELETE CASCADE su article_id cancella i commenti insieme al loro articolo, perché un commento non esiste senza articolo
  • la chiave primaria composta di likes impedisce di apprezzare due volte lo stesso articolo
  • utf8mb4 accetta tutti i caratteri, comprese le emoji che i visitatori non mancheranno di scrivere

Con Laravel, il modello fisico si scrive sotto forma di migrazioni invece che in SQL puro. Il ragionamento resta esattamente lo stesso.

E il diagramma delle classi?

Nei progetti software, i dati si modellano spesso anche con un diagramma delle classi UML. I due approcci si somigliano, ma non servono alla stessa cosa.

  • il modello concettuale descrive dei dati e i loro legami. Nient'altro
  • il diagramma delle classi descrive degli oggetti: i loro dati, ma anche i loro metodi, l'ereditarietà e le interfacce

Con un ORM, le cardinalità si ritrovano direttamente nel codice.

Cardinalità del modello Eloquent JPA
x,n / x,1 hasMany e belongsTo @OneToMany e @ManyToOne
x,1 / 0,1 hasOne e belongsTo @OneToOne
x,n / x,n belongsToMany @ManyToMany
x,n / x,n con proprietà belongsToMany e withPivot() un'entità dedicata con due @ManyToOne

L'ultima riga è importante. Appena un'associazione porta dati, come la data di APPREZZA, JPA richiede una vera entità per la tabella di associazione. Un @ManyToMany non sa salvare una colonna in più.

Cosa tenere a mente

La progettazione è uno strumento di discussione. Un diagramma che nessuno legge non serve a nulla. Un diagramma che fa dire a qualcuno «ah no, un commento può esistere senza account» ha appena fatto risparmiare una settimana.

Non si fanno tutti i diagrammi per ogni funzionalità. Un modulo di contatto non ha bisogno di un diagramma di sequenza. Una funzionalità che tocca i pagamenti sì.

Un modello vive con il progetto. Quando il database cambia, cambia anche il modello. Altrimenti mente, e un documento che mente è peggio di nessun documento.

Per approfondire

Un esercizio per finire. Prendi una funzionalità del tuo progetto attuale e scrivi la sua user story con due criteri di accettazione. Poi disegna la parte del modello dei dati che tocca. Se una cardinalità ti fa esitare, è una domanda da fare a qualcuno prima di scrivere codice.

Commenti (0)

Lascia un commento

Non hai effettuato l'accesso

Puoi commentare indicando il tuo nome e la tua email. Il messaggio sarà pubblicato dopo la moderazione. Con un account appare subito e resta modificabile.

La tua email non sarà pubblicata.

Ancora nessun commento

Sii il primo a commentare questo articolo!