Il backend: Python parla con il database
Finora abbiamo imparato a scrivere SQL in un client (MySQL Workbench, DBeaver). Nelle applicazioni reali, però, le query non le scrive un essere umano: le genera il codice. Il flusso standard è:
SQLite3 è un database embedded: non ha un server separato. Il database è un singolo file (.db) che vive nella stessa directory del progetto. È incluso nella libreria standard di Python: niente da installare. Perfetto per sviluppo, applicazioni desktop, progetti scolastici e prototipazione.
Il modulo sqlite3 di Python
Connessione, cursor e ciclo di vita
# ── 1. Connessione al file DB (viene creato se non esiste) ────────────── import sqlite3 conn = sqlite3.connect("scuola.db") # apre/crea il file scuola.db # ── 2. Il cursor: "penna" per eseguire comandi SQL ────────────────────── cursor = conn.cursor() # ── 3. Creazione tabella (se non esiste) ──────────────────────────────── cursor.execute(""" CREATE TABLE IF NOT EXISTS studenti ( id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT NOT NULL, cognome TEXT NOT NULL, anno INTEGER NOT NULL ) """) # ── 4. Salva le modifiche DDL/DML ─────────────────────────────────────── conn.commit() # ── 5. Chiudi la connessione quando hai finito ────────────────────────── conn.close()
| Metodo | Cosa fa |
|---|---|
connect("file.db") | Apre/crea il database. Con ":memory:" usa la RAM (DB temporaneo) |
cursor() | Crea un oggetto Cursor per eseguire comandi SQL |
execute(sql) | Esegue una singola istruzione SQL |
executemany(sql, lista) | Esegue la stessa istruzione per ogni elemento della lista |
fetchall() | Restituisce tutte le righe del risultato come lista di tuple |
fetchone() | Restituisce la prima riga, poi la successiva ad ogni chiamata |
commit() | Salva definitivamente le modifiche (DML) |
rollback() | Annulla le modifiche non ancora committate |
close() | Chiude la connessione e libera le risorse |
Inserimento e lettura dati
import sqlite3 # ── Inserimento di più studenti ────────────────────────────────────────── with sqlite3.connect("scuola.db") as conn: cursor = conn.cursor() studenti = [ ("Mario", "Rossi", 4), ("Giulia", "Bianchi", 3), ("Luca", "Verdi", 4), ] cursor.executemany( "INSERT INTO studenti (nome, cognome, anno) VALUES (?, ?, ?)", studenti ) conn.commit() print(f"Inseriti {cursor.rowcount} studenti") # ── Lettura con filtro ─────────────────────────────────────────────────── with sqlite3.connect("scuola.db") as conn: conn.row_factory = sqlite3.Row # righe accessibili come dict cursor = conn.cursor() anno_cercato = 4 cursor.execute( "SELECT * FROM studenti WHERE anno = ?", (anno_cercato,) # ← tupla con i parametri ) for row in cursor.fetchall(): print(f"{row['nome']} {row['cognome']} — Anno {row['anno']}")
Usare with sqlite3.connect(...) as conn: è il modo consigliato: in caso di eccezione fa automaticamente il rollback; se tutto va bene chiama il commit. Evita di dimenticarsi di close().
SQL Injection: l’attacco più diffuso nei DB web
La SQL Injection è nella OWASP Top 10 da quasi vent’anni. Si verifica quando un’applicazione concatena direttamente l’input dell’utente nella query SQL senza validazione. L’attaccante modifica la logica della query inserendo SQL arbitrario.
Esempio di codice vulnerabile
# ⛔ CODICE VULNERABILE — non fare mai così! username = input("Username: ") # l'utente digita: admin' -- password = input("Password: ") # Costruisce la query con concatenazione di stringhe query = "SELECT * FROM utenti WHERE username='" + username + \ "' AND password='" + password + "'" # Query risultante se username = "admin' --": # SELECT * FROM utenti WHERE username='admin' --' AND password='...' # Il -- commenta tutto il resto → password ignorata → accesso come admin! cursor.execute(query) # PERICOLOSISSIMO
Anatomia dell’attacco
| Input utente | Query generata | Effetto |
|---|---|---|
admin' -- | WHERE username='admin' --'... | Login senza password |
' OR '1'='1 | WHERE username='' OR '1'='1' | Restituisce tutti gli utenti |
'; DROP TABLE utenti; -- | ...; DROP TABLE utenti; -- | Elimina l’intera tabella |
' UNION SELECT username, password FROM utenti -- | UNION con tabella credenziali | Data leak completo |
La soluzione: Prepared Statement (query parametrizzate)
# ✅ CODICE SICURO — usa sempre i parametri (?) username = input("Username: ") password = input("Password: ") # I ? sono segnaposto: sqlite3 li sostituisce in modo sicuro cursor.execute( "SELECT * FROM utenti WHERE username = ? AND password = ?", (username, password) # ← parametri come tupla SEPARATA dalla query ) # Anche se username = "admin' --", sqlite3 lo tratta come stringa letterale: # cerca username uguale a "admin' --", non trova nulla → login fallisce ✓ utente = cursor.fetchone() if utente: print("Accesso consentito") else: print("Credenziali errate")
Con i prepared statement, la query viene compilata prima di inserire i parametri. Il DBMS sa già quale parte è “codice SQL” e quale è “dato”. Anche se l’utente inserisce del SQL, viene trattato come dato puro — nessun rischio di esecuzione. Regola d’oro: mai concatenare stringhe per costruire query SQL.
Flask: un’applicazione web minimale con SQLite3
Flask è un micro-framework Python per creare applicazioni web. Con poche righe di codice puoi avere route, template HTML e accesso al database.
pip install flaskStruttura del progetto
├── app.py ← logica Flask + query SQL
├── scuola.db ← database SQLite3
└── templates/
├── studenti.html ← template Jinja2 (lista)
└── aggiungi.html ← template Jinja2 (form)
app.py — Backend completo
from flask import Flask, render_template, request, redirect, url_for import sqlite3 app = Flask(__name__) DB = "scuola.db" # ── Helper: connessione con row_factory ───────────────────────────────── def get_db(): conn = sqlite3.connect(DB) conn.row_factory = sqlite3.Row # righe come dizionari return conn # ── Init DB all'avvio ──────────────────────────────────────────────────── def init_db(): with get_db() as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS studenti ( id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT NOT NULL, cognome TEXT NOT NULL, anno INTEGER NOT NULL ) """) # ── Route GET: mostra tutti gli studenti ──────────────────────────────── @app.route("/") def lista_studenti(): with get_db() as conn: studenti = conn.execute( "SELECT * FROM studenti ORDER BY cognome" ).fetchall() return render_template("studenti.html", studenti=studenti) # ── Route POST: aggiunge uno studente dal form ─────────────────────────── @app.route("/aggiungi", methods=["GET", "POST"]) def aggiungi(): if request.method == "POST": nome = request.form["nome"] cognome = request.form["cognome"] anno = request.form["anno"] # ✅ Prepared statement — NESSUNA concatenazione! with get_db() as conn: conn.execute( "INSERT INTO studenti (nome, cognome, anno) VALUES (?, ?, ?)", (nome, cognome, anno) ) return redirect(url_for("lista_studenti")) return render_template("aggiungi.html") if __name__ == "__main__": init_db() app.run(debug=True)
templates/studenti.html — Visualizzazione dinamica con Jinja2
<!-- templates/studenti.html --> <!DOCTYPE html> <html><body> <h1>Elenco Studenti</h1> <a href="{{ url_for('aggiungi') }}">+ Aggiungi studente</a> <table> <tr><th>ID</th><th>Nome</th><th>Cognome</th><th>Anno</th></tr> {% for s in studenti %} <tr> <td>{{ s['id'] }}</td> <td>{{ s['nome'] }}</td> <td>{{ s['cognome'] }}</td> <td>{{ s['anno'] }}ª</td> </tr> {% endfor %} </table> </body></html>
templates/aggiungi.html — Form di inserimento
<!-- templates/aggiungi.html --> <h1>Aggiungi Studente</h1> <!-- action punta alla route /aggiungi, metodo POST --> <form action="{{ url_for('aggiungi') }}" method="POST"> <label>Nome: <input type="text" name="nome" required> </label> <label>Cognome: <input type="text" name="cognome" required> </label> <label>Anno (1-5): <input type="number" name="anno" min="1" max="5" required> </label> <button type="submit">Salva</button> </form>
Modificare dati: UPDATE via form pre-popolato
# ── Route GET/POST per modificare uno studente esistente ───────────────── @app.route("/modifica/<int:id_studente>", methods=["GET", "POST"]) def modifica(id_studente): with get_db() as conn: if request.method == "POST": # Aggiorna il record — prepared statement obbligatorio! conn.execute( "UPDATE studenti SET nome=?, cognome=?, anno=? WHERE id=?", (request.form["nome"], request.form["cognome"], request.form["anno"], id_studente) ) return redirect(url_for("lista_studenti")) # GET: carica dati esistenti per pre-popolare il form studente = conn.execute( "SELECT * FROM studenti WHERE id = ?", (id_studente,) ).fetchone() return render_template("modifica.html", s=studente)
Dopo ogni operazione DML (INSERT, UPDATE, DELETE) si usa redirect(url_for(...)) per tornare alla lista. Questo evita il problema del “doppio invio” se l’utente aggiorna la pagina dopo un POST (il browser non ri-invia i dati del form).
Riepilogo sicurezza: checklist per un’app DB-web
| Rischio | Soluzione | In Python |
|---|---|---|
| SQL Injection | Prepared statement sempre | execute(sql, (param,)) |
| Password in chiaro | Hash con salt | werkzeug.security.generate_password_hash |
| XSS (Cross-Site Scripting) | Escape automatico dell’output | Jinja2 fa l’escape di default con {{ var }} |
| CSRF (Cross-Site Request Forgery) | Token CSRF nei form | Flask-WTF o token manuale in sessione |
| Dati sensibili in git | File .env + .gitignore | python-dotenv, mai hardcoded secrets |
- Il modulo
sqlite3è nella libreria standard Python: nessuna installazione, un solo file.db, perfetto per sviluppo e prototipazione - Il ciclo tipico è:
connect()→cursor()→execute()→fetchall()→commit()→close(). Meglio usare il context managerwith - SQL Injection: attacco che inietta SQL nell’input per alterare le query. Causa data leak, accessi non autorizzati, perdita dati
- Prevenzione: sempre prepared statement con
?come segnaposto. Mai concatenare stringhe per costruire query SQL - Flask permette di collegare form HTML a query SQL in poche righe: route GET per mostrare dati, route POST per riceverli e inserirli/aggiornarli
- Jinja2 genera HTML dinamico con
{% for %}e{{ variabile }}; il pattern POST-Redirect-GET previene il doppio invio del form