modulo 04 / lezione 25
Architettura mentale di un'applicazione
Database: quando serve, quale tipo, come si modella
Decidere se il progetto ha davvero bisogno di persistenza e quale forma di dato richiede.
Database: quando serve, quale tipo, come si modella
Non tutto ciò che persiste richiede un database. Non tutti i database fanno la stessa cosa. La modellazione dei dati non è dettaglio implementativo: è decisione architetturale che condiziona l’app per anni. Sapere quando, quale, come — prima di chiedere all’AI — significa partire bene invece di aggiustare male.
Perché questa lezione conta
Il modello AI tende a proporre Postgres + ORM completo per ogni progetto, anche per quelli che non avrebbero bisogno di un database. Oppure suggerisce Mongo “perché flessibile” quando i dati sono chiaramente relazionali. Oppure propone schemi senza vincoli, indici, foreign key. Sapere riconoscere queste scelte prima di accettarle è il filtro principale fra prototipo solido e prototipo che esplode al primo dato reale.
Obiettivo operativo
Alla fine devi saper:
- decidere se serve un DB o basta una soluzione più semplice;
- scegliere fra relazionale, documentale, key-value in base al dato;
- modellare un piccolo dominio con entità, attributi, relazioni, vincoli, indici.
Nucleo della lezione
1. Quando serve un database
Non sempre. Alternative valide a seconda del caso:
- Costanti hardcoded: dati che non cambiano (es. lista regioni italiane). File JSON statico, importato.
- File markdown / CSV: contenuti editoriali, blog, dataset piccolo. Versionati con git.
- localStorage / IndexedDB: preferenze locali individuali, dati offline-first per single user.
- File JSON su disco: piccoli progetti single-user con scrittura saltuaria. Non scalabile, ma ok per prototipi.
- SQLite file: piccola app local-first, anche con scrittura. Veramente sottovalutato per progetti piccoli/medi.
Serve database vero quando:
- più utenti accedono ai dati simultaneamente;
- volume cresce nel tempo (migliaia/milioni di record);
- query complesse (join, aggregazioni);
- transazioni atomiche (più operazioni che devono andare tutte o nessuna);
- audit, backup, replica.
Per molti vibe-coding project iniziali, SQLite o un BaaS (Supabase, PocketBase, Turso) bastano. Postgres self-hosted dal giorno 1 è spesso premature.
2. Tre famiglie di database
Relazionale (SQL): tabelle con schema fisso, righe, colonne. Relazioni tramite foreign key. Forza: integrità, query potenti (JOIN, aggregazioni), transazioni ACID. Esempi: Postgres, MySQL/MariaDB, SQLite.
Quando: dati strutturati con relazioni chiare. La maggior parte dei progetti.
Documentale (NoSQL document): collezioni di documenti JSON, schema flessibile. Forza: documenti annidati, evoluzione schema rapida. Limiti: join meno naturali, integrità referenziale a carico dell’app. Esempi: MongoDB, Firestore.
Quando: documenti naturalmente annidati (es. articolo con commenti annidati), schema che cambia frequentemente, lettura by-id più frequente di query complesse.
Key-value: chiave → valore semplice. Estremamente veloce. Forza: latenza bassa, semplicità. Limiti: niente query complesse. Esempi: Redis, DynamoDB, Cloudflare KV.
Quando: cache, sessioni, rate limiting, contatori, fila di lavoro semplice.
Anti-pattern: scegliere documentale “perché flessibile” quando i dati sono relazionali (utenti, ordini, prodotti). Risultato: app che reimplementa join a mano, lentamente, con bug.
Default ragionevole per la maggior parte dei progetti: relazionale (Postgres o SQLite). Aggiungi key-value (Redis) per cache/sessione quando serve.
3. Modellazione minima
Cinque elementi:
Entità: cose che esistono nel dominio. User, Order, Product, Comment.
Attributi: caratteristiche di ogni entità. User ha email, name, created_at.
Relazioni: come entità si collegano. Order appartiene a User (1:N). Product fa parte di N Orders (N:M tramite OrderItem).
Vincoli: regole sull’integrità dei dati.
- NOT NULL: campo obbligatorio.
- UNIQUE: valore univoco (es. email).
- CHECK: regola complessa (es. age ≥ 18).
- FOREIGN KEY: integrità referenziale (cancello user → cosa fanno i suoi orders? CASCADE / SET NULL / RESTRICT).
Indici: accelerano lettura su colonne spesso filtrate. Regola: indicizza colonne usate in WHERE, ORDER BY, JOIN. Non sovra-indicizzare: ogni indice rallenta le scritture.
Migrazioni: cambiamenti di schema versionati. Mai modificare schema di produzione manualmente. Ogni cambio = file di migrazione, testato, reversibile (idealmente).
Dati di esempio (seed): dataset minimo per sviluppo. Versionato, riproducibile.
Procedura pratica
Per progettare schema iniziale:
- Lista entità: 3-7 per app piccola. Se > 10, decomponi feature.
- Per ogni entità: 5-10 attributi essenziali. Tipi (string, int, bool, date, …).
- Disegna relazioni: 1:1, 1:N, N:M. Per N:M serve tabella di join.
- Aggiungi vincoli: NOT NULL su campi obbligatori, UNIQUE su naturali (email, slug), FK con politica delete.
- Identifica indici: cosa interroghi spesso? user.email, post.slug, message.created_at.
- Domande aperte: cose che non sai (es. “soft delete o hard delete?”). Risolvile prima di codare.
Laboratorio guidato
Modello dati per app to-do condivise (lezione 23 estesa).
Entità:
User: id, email (unique, not null), name, created_at.Couple: id, name, created_at.CoupleMember: id, couple_id (FK), user_id (FK), role (enum ‘owner’, ‘member’), joined_at. UNIQUE (couple_id, user_id).Task: id, couple_id (FK), title (not null), description (nullable), completed_at (nullable timestamp), created_by (FK user), created_at.
Relazioni:
- User : Couple = N:M tramite CoupleMember.
- Task : Couple = N:1 (un task appartiene a una coppia).
- Task : User = N:1 (created_by).
Vincoli:
- Cancellazione User: ON DELETE RESTRICT (non cancello user con task) o SET NULL (created_by diventa null).
- Cancellazione Couple: ON DELETE CASCADE (cancello task della coppia).
Indici:
- task.couple_id (per
WHERE couple_id = Xmolto frequente); - task.completed_at (per filtri “non completate”);
- couple_member.user_id (per “le coppie di Carlo”).
Domande aperte:
- soft delete o hard delete dei task? (audit?)
- task ricorrenti? (out of scope iniziale, lasciato a v2);
- supporto inviti pending? (out of scope iniziale).
Disegna lo schema (testuale o tool tipo dbdiagram.io) prima di chiedere implementazione.
Output atteso
Schema dati di una pagina: lista entità + attributi tipizzati + relazioni con cardinalità + vincoli + indici + domande aperte. Allegato come contesto al prompt iniziale.
Studio passo passo
Studia questa lezione in tre passaggi, senza correre subito al prompt o al codice.
- Prima lettura: chiarisci l’obiettivo: decidere se il progetto ha davvero bisogno di persistenza e quale forma di dato richiede. Se non riesci a riscriverlo in una frase tua, rileggi il nucleo della lezione.
- Seconda lettura: trasforma i contenuti in domande operative:
- Quando bastano file, local storage o dati statici e quando serve un database reale.
- Relazionale, documentale e key-value come modelli mentali, senza trasformare la scelta in religione.
- Modellazione minima: entità, attributi, relazioni, vincoli, indici, migrazioni e dati di esempio.
- Applicazione: svolgi il laboratorio: disegnare il modello dati di una piccola app con tre entità e segnare relazioni e campi obbligatori. L’output da ottenere è: uno schema dati iniziale con domande aperte da risolvere prima dell’implementazione.
Prima di passare alla lezione successiva, controlla di avere un artefatto concreto: una tabella, un prompt, una checklist, un mini report, un diff commentato o una decisione scritta. Nel vibe coding la comprensione resta astratta finché non produce una traccia verificabile.
Checklist di verifica
- so quando NON serve un database vero;
- ho default chiaro fra relazionale/documentale/key-value e so quando deviare;
- modello entità + relazioni + vincoli + indici prima del codice;
- ho gestione esplicita di “cosa succede quando cancello un User”;
- niente schema che ignori UNIQUE, FK, NOT NULL.
Errori comuni
Postgres + ORM completo per progetto da 100 record. Sovraingegnerizzato. SQLite + query a mano è spesso sufficiente.
MongoDB per dati relazionali. “Per flessibilità” → mesi di workaround per join.
Niente foreign key (“è più veloce”). Falso. Performance di FK è marginale; la perdita di integrità ti costa molto.
Niente UNIQUE su email/username. Risultato: due utenti stesso email, account che si scontrano.
Niente indici → query lente non appena dati crescono. Aggiungi indici dove filtri/ordini.
Schema senza migrazioni versionate. Modifiche dirette → produzione divergente da sviluppo, deploy rotti.
“Salviamo total per non ricalcolarlo”. Stato derivato salvato a parte = bug di sincronizzazione. Calcola al volo o usa view materializzata gestita esplicitamente.
Prompt di lavoro
Sto modellando dati per: [descrizione app]
Persone/entità del dominio: [lista]
Operazioni principali: [es. lista, dettaglio, crea, aggiorna, cancella, ricerca, aggregazioni]
Volume stimato: [es. 1000 utenti, 100K task]
Vincoli: [es. on-premise, hosted, single-user/multi-tenant]
Voglio in output:
1. Decisione "serve DB?" con motivazione (vs alternative tipo file, KV, BaaS).
2. Scelta famiglia (relazionale / documentale / KV) con motivazione.
3. Schema:
- entità (3-7) con attributi tipizzati
- relazioni con cardinalità
- vincoli (NOT NULL, UNIQUE, CHECK, FK con policy delete)
- indici raccomandati per query principali
4. Domande aperte da risolvere prima di scrivere codice.
Non scrivere ancora codice migrazioni. Voglio prima validare lo schema.
Esercizio finale
Prendi un progetto in produzione di tua conoscenza (tuo o studiato). Ricostruisci a posteriori il suo schema dati. Quali decisioni faresti diversamente oggi? Identifica almeno una violazione di anti-pattern dalla sezione “errori comuni” — quasi certamente ce n’è una. Quel difetto è il tipo di errore che il tuo prossimo prompt al modello deve evitare.