In questa lezione impari a
- capire perché serve un database e cos'è SQLite
- leggere e scrivere dati da Flask con il modulo sqlite3
- usare sempre i segnaposto ? nelle query
- fare ricerche con LIKE e ordinare con ORDER BY
- eseguire le operazioni CRUD dalle route
Finora gli annunci stavano in una lista Python: riavviando il server spariscono. Serve un database: conserva i dati in modo permanente, li condivide tra tutti gli utenti e permette ricerche veloci. Useremo l'SQL che conoscete da Informatica, con le stesse query che scrivete in PHP con mysqli.
Il kit della lezione
Al primo avvio nel terminale compare Database creato: …bacheca.db. Pubblicate un annuncio, fermate il server (Ctrl + C), riavviatelo: l'annuncio è ancora lì. Provate anche la ricerca e il pulsante "Elimina".
SQLite: un database in un file
| MySQL (PHP) | SQLite (oggi) | |
|---|---|---|
| Dove stanno i dati | in un server (XAMPP, Aruba…) | in un file: bacheca.db |
| Cosa installare | il server MySQL | niente: sqlite3 è già dentro Python |
| Il linguaggio | SQL | SQL (quasi identico) |
| Adatto a | siti con tanti utenti contemporanei | sviluppo, app piccole e medie, telefoni |
SQLite è il database più usato al mondo: c'è in ogni telefono, in ogni browser, in tantissime app. Per guardare dentro bacheca.db usate l'estensione SQLite Viewer di VS Code (basta un clic sul file) oppure il programma gratuito DB Browser for SQLite.
I file nuovi del kit
flask5-04-database/
├── app.py ← le route ora usano le funzioni di db.py
├── db.py ← NUOVO: tutte le funzioni per parlare con il database
├── schema.sql ← NUOVO: la struttura delle tabelle e i dati di esempio
├── bacheca.db ← viene creato al primo avvio (non si consegna)
├── templates/
└── static/
schema.sql
CREATE TABLE annunci (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titolo TEXT NOT NULL,
testo TEXT NOT NULL,
categoria TEXT NOT NULL DEFAULT 'avviso',
data TEXT NOT NULL DEFAULT (date('now', 'localtime'))
);
È SQL normale. Due dettagli di SQLite: i tipi principali sono pochi (INTEGER, REAL, TEXT), e le date si salvano come testo nel formato "AAAA-MM-GG", che si ordina e si confronta correttamente. DEFAULT (date('now', 'localtime')) mette da solo la data di oggi se non la indichiamo.
db.py, le quattro funzioni
def connetti():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row # le righe si leggono come dizionari
return conn
def leggi_tutti(sql, parametri=()): # SELECT con tante righe → lista
conn = connetti()
righe = conn.execute(sql, parametri).fetchall()
conn.close()
return righe
def leggi_uno(sql, parametri=()): # SELECT con una riga → riga oppure None
...fetchone()...
def esegui(sql, parametri=()): # INSERT, UPDATE, DELETE
conn = connetti()
cursore = conn.execute(sql, parametri)
conn.commit() # senza commit NON si salva niente!
conn.close()
return cursore.lastrowid # l'id della riga appena inserita
Se avete fatto PHP, il confronto è diretto:
| PHP con mysqli | Python con sqlite3 (db.py) |
|---|---|
mysqli_connect(...) |
connetti() |
mysqli_prepare + bind_param + execute |
conn.execute(sql, parametri) |
mysqli_fetch_all(..., MYSQLI_ASSOC) |
leggi_tutti(...) |
mysqli_fetch_assoc(...) |
leggi_uno(...) |
mysqli_insert_id($conn) |
il valore restituito da esegui(...) |
Con row_factory = sqlite3.Row ogni riga si legge per nome della colonna: riga["titolo"] in Python, riga.titolo nei template. Per questo i template non sono cambiati rispetto alla lezione 3: prima ricevevano dizionari, ora righe che si comportano come dizionari.
crea_database() viene chiamata all'avvio di app.py: se bacheca.db non esiste, lo crea eseguendo schema.sql. Volete ripartire da zero? Fermate il server e cancellate bacheca.db.
Le route con il database
Leggere: SELECT
def trova_annuncio(id):
return leggi_uno("SELECT * FROM annunci WHERE id = ?", (id,))
@app.route("/")
def home():
annunci = leggi_tutti("SELECT * FROM annunci ORDER BY data DESC, id DESC")
return render_template("index.html", annunci=annunci)
ORDER BY data DESC, id DESC: prima i più recenti; a parità di data, prima l'ultimo inserito.
Il punto interrogativo: i segnaposto
leggi_uno("SELECT * FROM annunci WHERE id = ?", (id,))
Il ? è un segnaposto: il valore lo inserisce sqlite3, trattandolo sempre come un dato e mai come codice SQL. I parametri vanno passati come tupla: con un solo valore serve la virgola, (id,).
Mai costruire una query unendo stringhe, come
f"SELECT * FROM annunci WHERE titolo = '{titolo}'". Se l'utente scrive come titolo' OR '1'='1, la query cambia significato: è la SQL injection, uno degli attacchi più diffusi al mondo. Lo proverete sul serio nel laboratorio di sicurezza. Con i?è impossibile.
Cercare: LIKE
q = request.args.get("q", "").strip()
if q:
annunci = leggi_tutti(
"SELECT * FROM annunci WHERE titolo LIKE ? OR testo LIKE ? ORDER BY data DESC, id DESC",
(f"%{q}%", f"%{q}%"))
% vuol dire "qualunque testo": %museo% trova "museo" in qualsiasi punto. I % si mettono nel valore, non nella query, e il ? resta. Il form di ricerca è un form GET: il testo finisce nell'indirizzo (/?q=museo), così una ricerca si può copiare e condividere.
Inserire: INSERT
nuovo_id = esegui(
"INSERT INTO annunci (titolo, testo, categoria) VALUES (?, ?, ?)",
(titolo, testo, categoria))
flash("Annuncio pubblicato!", "successo")
return redirect(url_for("annuncio", id=nuovo_id))
id e data non li scriviamo: li mette il database (AUTOINCREMENT e DEFAULT). esegui restituisce l'id appena creato, che usiamo per il redirect.
Eliminare: DELETE
@app.route("/annuncio/<int:id>/elimina", methods=["POST"])
def elimina(id):
if trova_annuncio(id) is None:
abort(404)
esegui("DELETE FROM annunci WHERE id = ?", (id,))
flash("Annuncio eliminato", "successo")
return redirect(url_for("home"))
Solo POST: nel template il pulsante sta dentro un <form method="post">.
CRUD
Le quattro operazioni che fa ogni applicazione gestionale:
| Lettera | Operazione | SQL | Nella bacheca |
|---|---|---|---|
| C | Create | INSERT |
/nuovo ✔ |
| R | Read | SELECT |
/, /annuncio/<id>, ricerca ✔ |
| U | Update | UPDATE |
tocca a voi (missione 4) |
| D | Delete | DELETE |
/annuncio/<id>/elimina ✔ |
Gli errori più comuni
| Errore | Causa |
|---|---|
sqlite3.OperationalError: no such table |
il file .db è vecchio o vuoto: cancellatelo e riavviate |
sqlite3.OperationalError: near "…": syntax error |
errore di scrittura nella query SQL |
Incorrect number of bindings supplied |
il numero di ? non coincide con i valori nella tupla (o manca la virgola in (id,)) |
| Le modifiche non restano | manca conn.commit() (in esegui c'è già) |
database is locked |
il file è aperto in scrittura da DB Browser: chiudetelo |
Missioni
⭐ Missione 1 – Guardate dentro
Aprite bacheca.db con SQLite Viewer (o DB Browser). Pubblicate un annuncio dal sito e verificate che compaia nella tabella. Poi, dalla scheda Execute SQL di DB Browser, eseguite una SELECT che conta gli annunci per categoria (GROUP BY).
⭐ Missione 2 – Dal più vecchio
Aggiungete al form di ricerca un menu "Ordina" con Più recenti e Più vecchi, e usatelo nella query.
Aiutino
L'ordinamento non si può passare con un ? (i segnaposto valgono solo per i valori). Scegliete in Python tra due stringhe fisse: ordine = "ASC" if request.args.get("ordine") == "vecchi" else "DESC", e solo dopo inseritela nella query. Si può fare perché ordine può valere solo una delle due parole decise da voi.
⭐⭐ Missione 3 – Statistiche
Create la pagina /statistiche con il numero totale di annunci e una tabella con quanti annunci ci sono per categoria, calcolati dal database con COUNT(*) e GROUP BY.
⭐⭐⭐ Missione 4 – La U di CRUD
Aggiungete /annuncio/<id>/modifica con il form già compilato e la query UPDATE annunci SET titolo = ?, testo = ?, categoria = ? WHERE id = ?. Nella pagina dell'annuncio mettete un pulsante "Modifica".
⭐⭐⭐ Missione 5 – I commenti
Aggiungete la tabella commenti (id, annuncio_id, autore, testo, data), collegata agli annunci con una chiave esterna. Nella pagina di un annuncio mostrate i suoi commenti e un piccolo form per aggiungerne uno.
Aiutino
Aggiungete il CREATE TABLE in schema.sql e cancellate bacheca.db perché venga ricreato. La colonna di collegamento: annuncio_id INTEGER NOT NULL REFERENCES annunci(id). I commenti di un annuncio: SELECT * FROM commenti WHERE annuncio_id = ? ORDER BY data.