Scarica la lezione con esercizi e soluzioni (PDF, 9 pagine)
Scarica laboratorio Python e schema SQLite (ZIP didattico) — Usare solo su un database di prova: setup.sql elimina le due tabelle. Il laboratorio Python usa dati in memoria.
Informatica · Scuola secondaria di secondo grado · Triennio
Il problema: tre kit spariti, ma nessuna prenotazione
Il laboratorio della scuola ha dieci kit di robotica disponibili. Una classe ne prenota tre. Il programma deve fare due cose: ridurre la disponibilità da dieci a sette e registrare la prenotazione. Se esegue soltanto il primo passaggio, il magazzino risulta più vuoto, ma non sappiamo a chi sono destinati i kit.
Non basta che ciascuna istruzione SQL sia corretta. Le due modifiche rappresentano una sola operazione per la scuola e devono essere gestite insieme. Una transazione delimita proprio questa unità di lavoro: le modifiche che decidiamo di confermare diventano effettive insieme; se rinunciamo all’operazione prima della conferma, possiamo annullarle insieme. È l’idea di atomicità, spiegata anche nel tutorial PostgreSQL 18, §3.4, nei paragrafi iniziali sull’operazione “all-or-nothing” [1].
La transazione non indovina però quali passaggi avevamo intenzione di eseguire. Se il programma ne dimentica uno e conferma comunque, può rendere definitivo un risultato sbagliato. Nella lezione impareremo quindi sia i comandi di controllo sia i controlli che spettano al programma.
Prima di cominciare
Servono le nozioni di tabella, chiave primaria, SELECT, INSERT, UPDATE e condizione WHERE. Il percorso è pensato per una lezione con laboratorio, indicativamente 90 minuti, da adattare alla classe. Gli obiettivi sono distinguere conferma e annullamento, prevedere lo stato dei dati e riconoscere un errore che lascia aperta una transazione.
Gli esempi usano SQLite, una sola connessione e quantità intere positive. Non simulano accessi contemporanei, guasti hardware o un servizio di prenotazione pronto per l’uso reale. Il file Python allegato usa soltanto la libreria standard, crea un database temporaneo in memoria e non si collega a servizi esterni. Il database in memoria scompare quando il programma termina: serve a osservare gli stati, non a dimostrare la conservazione dei dati dopo un riavvio.
Tre comandi, tre decisioni
Con BEGIN; apriamo un blocco esplicito. Con COMMIT; lo confermiamo. Con ROLLBACK; annulliamo le modifiche non ancora confermate di quel blocco. La documentazione SQLite, §2 “Transactions”, descrive questo ciclo; §2.3 distingue le transazioni esplicite da quelle implicite [2].
BEGIN;
-- Qui inseriamo le modifiche che appartengono alla stessa operazione.
COMMIT;
L’alternativa finale è ROLLBACK;, non un comando da aggiungere dopo COMMIT;. Un annullamento non è una macchina del tempo: dopo una conferma riuscita non cancella la transazione precedente. Per correggere dati già confermati occorre una nuova operazione, progettata in base al problema.
Anche senza scrivere BEGIN, SQLite esegue le istruzioni che accedono ai dati dentro transazioni. Il punto è quali istruzioni appartengono allo stesso blocco. Nella modalità automatica usata dal nostro laboratorio, un UPDATE e un INSERT separati non diventano una sola operazione soltanto perché sono vicini nel file. Alcune librerie gestiscono automaticamente l’apertura dei blocchi: non bisogna attribuire al database il comportamento particolare di un’interfaccia [1, nota sulle librerie client; 2, §2.3].
Prepariamo dati piccoli e leggibili
Questo è il contenuto del file setup.sql. Eseguirlo soltanto in un database di prova: elimina le due tabelle se esistono. Se una precedente transazione è rimasta aperta, chiuderla prima del ripristino. Il laboratorio Python crea invece una connessione nuova per ogni scenario.
PRAGMA foreign_keys = ON;
DROP TABLE IF EXISTS prenotazioni;
DROP TABLE IF EXISTS scorte;
CREATE TABLE scorte (
id INTEGER PRIMARY KEY,
disponibili INTEGER NOT NULL CHECK (disponibili >= 0)
);
CREATE TABLE prenotazioni (
id INTEGER PRIMARY KEY,
kit_id INTEGER NOT NULL REFERENCES scorte(id),
quantita INTEGER NOT NULL CHECK (quantita > 0)
);
INSERT INTO scorte VALUES (1, 10), (2, 4);
Il kit 1 parte da dieci unità; il kit 2 da quattro. La tabella delle prenotazioni è vuota. CHECK impone il vincolo indicato; la chiave primaria impedisce due prenotazioni con lo stesso identificativo. Per i collegamenti tra tabelle attiviamo le chiavi esterne sulla connessione SQLite con il PRAGMA iniziale. Sono scelte di schema utili al laboratorio, non un modello completo di magazzino: mancano, per esempio, le restituzioni e i dati delle classi.
Per leggere lo stato usiamo due interrogazioni:
SELECT id, disponibili FROM scorte ORDER BY id;
SELECT id, kit_id, quantita FROM prenotazioni ORDER BY id;
I risultati sono coppie (id, disponibili) e terne (id, kit_id, quantita). Scrivere i risultati attesi prima di eseguire i comandi aiuta a distinguere ciò che immaginiamo da ciò che il database ha davvero registrato.
Una prenotazione riuscita
Partiamo dai dati iniziali e prenotiamo tre unità del kit 1:
BEGIN;
UPDATE scorte
SET disponibili = disponibili - 3
WHERE id = 1 AND disponibili >= 3;
-- Il programma deve verificare che sia stata modificata esattamente una riga.
INSERT INTO prenotazioni (id, kit_id, quantita)
VALUES (101, 1, 3);
COMMIT;
Con i dati iniziali l’aggiornamento modifica una riga. Alla fine le scorte sono (1, 7) e (2, 4); la prenotazione è (101, 1, 3). I tre kit non sono scomparsi: sono associati alla prenotazione 101.
Il commento sul controllo della riga è importante: un commento SQL non esegue un controllo. Il blocco mostrato illustra il caso riuscito con questi dati. Un programma deve interrompere il percorso e annullare il blocco se l’aggiornamento non trova la riga prevista. Il file laboratorio.py allegato rende quel controllo eseguibile.
Se interrogassimo i dati dalla stessa connessione prima di COMMIT, vedremmo già le nostre modifiche. Leggere un valore aggiornato nella propria connessione non dimostra quindi che sia stato confermato. Non studiamo qui ciò che vedrebbero altre connessioni, che riguarda anche l’isolamento.
Cambiare idea prima della conferma
Ripristiniamo i dati iniziali. Proviamo lo stesso aggiornamento e lo stesso inserimento, ma concludiamo con ROLLBACK; al posto di COMMIT;. Le scorte tornano a (1, 10) e (2, 4) e la tabella delle prenotazioni torna vuota.
Non è stato annullato soltanto l’ultimo comando: sono state annullate entrambe le modifiche del blocco. Questo è utile quando un controllo applicativo fallisce o l’utente rinuncia prima della conferma. Non equivale a ripristinare un backup e non riguarda le transazioni già concluse [2, §2].
L’insidia: un errore non annulla sempre l’intero blocco
Supponiamo di preparare una prenotazione con identificativo 101 già presente. Se il programma riduce le scorte e poi prova a inserire di nuovo 101, l’inserimento viola la chiave primaria.
In SQLite, per le violazioni di chiave primaria e di CHECK considerate qui, il comportamento ordinario è ABORT: fallisce l’istruzione che viola il vincolo, ma le modifiche delle istruzioni precedenti restano nella transazione aperta. Non confondere il nome della politica ABORT con un annullamento completo del blocco. La documentazione “The ON CONFLICT Clause”, voce “ABORT”, precisa questa distinzione [3].
Se il programma ignora l’errore e manda COMMIT, può confermare la riduzione delle scorte senza la nuova prenotazione. La transazione ha eseguito la conferma richiesta: è il programma ad aver scelto di confermare un lavoro incompleto.
Nel nostro laboratorio la regola applicativa è questa: se un passaggio obbligatorio fallisce, non confermare; se la transazione è ancora attiva, eseguire ROLLBACK. In Python controlliamo con.in_transaction prima dell’annullamento [5, attributo Connection.in_transaction]. Altri errori o diverse politiche possono avere conseguenze differenti: SQLite ne parla in “Response To Errors Within A Transaction”, §3 [2]. Non trasferiamo automaticamente queste regole a PostgreSQL, che gestisce diversamente lo stato di un blocco dopo un errore [1, ultimo paragrafo].
Zero righe modificate non significa errore SQL
Proviamo a sottrarre dodici kit quando ne abbiamo dieci:
UPDATE scorte
SET disponibili = disponibili - 12
WHERE id = 1 AND disponibili >= 12;
La condizione è falsa, quindi nessuna riga viene modificata. L’istruzione non genera per questo un errore [4, §2]. Se inserissimo comunque una prenotazione da dodici, i vincoli del nostro schema non basterebbero a collegare la registrazione alla diminuzione delle scorte. La chiave esterna verifica l’esistenza del kit, non il corretto conteggio delle disponibilità.
Nel programma usiamo il numero di righe modificate: dopo l’UPDATE, cur.rowcount deve valere 1 [5, attributo Cursor.rowcount]. Se vale 0, manca il kit richiesto oppure la quantità disponibile non basta; in entrambi i casi questo tentativo va annullato. Per un’applicazione reale servirebbero anche la validazione degli input, la gestione della concorrenza e degli eventuali errori di conferma. Il nostro esercizio isola un problema alla volta.
Che cosa garantisce l’atomicità, e che cosa no
L’atomicità riguarda l’unità della conferma o dell’annullamento. Non rende corretto un algoritmo sbagliato e non sostituisce i vincoli di schema. La correttezza della prenotazione richiede anche una quantità valida, un aggiornamento riuscito, una registrazione riuscita e una decisione finale coerente.
Si incontrano spesso le quattro lettere ACID: atomicità, coerenza rispetto alle regole dei dati, isolamento tra operazioni concorrenti e durabilità delle modifiche confermate. Qui osserviamo soprattutto la prima. Un database in memoria e una sola connessione non dimostrano né il comportamento con più utenti né la resistenza ai guasti. Le garanzie di conservazione dipendono anche dalla configurazione e dall’ambiente: non si disattivano i meccanismi di protezione per poi aspettarsi le stesse garanzie [2, §2.3, nota su journal_mode=OFF].
Come svolgere il laboratorio
Aprire una cartella di prova ed eseguire python3 laboratorio.py con Python 3, senza l’opzione -O. Su Python 3.12 e successivi il file imposta esplicitamente anche LEGACY_TRANSACTION_CONTROL: insieme a isolation_level=None permette di osservare i comandi SQL espliciti senza aperture implicite del driver [5, “Transaction control via the isolation_level attribute”]. Il programma non chiede dati e non scrive un database permanente. Crea scenari indipendenti, stampa gli stati e verifica i risultati con asserzioni. Per ogni scenario, prima dell’esecuzione annotare la disponibilità attesa, le righe di prenotazione attese e se resta aperta una transazione.
Non incollare tutti gli scenari SQL uno dopo l’altro nello stesso database senza ripristinare i dati: un risultato dipende dal suo stato iniziale. Non usare il file di ripristino su un archivio reale.
Sei esercizi: prevedi, esegui, spiega
Ogni esercizio parte da una connessione nuova con il nostro schema: scorte (1, 10) e (2, 4), prenotazioni vuote. Il kit 2 non viene modificato e deve restare a quattro unità. I frammenti Python presuppongono import sqlite3 e una connessione chiamata con, creata da nuova_connessione() nel file allegato. Le quantità sono interi positivi già stabiliti dall’esercizio.
Esercizio 1 — La prenotazione accettata
Scrivi una transazione che riduca di tre le unità disponibili del kit 1, registri (101, 1, 3) e confermi. Controlla il numero di righe modificate prima dell’inserimento. Prevedi le due tabelle dopo la conferma.
Esercizio 2 — La classe rinuncia
Inserisci (102, 1, 4) e sottrai quattro unità dal kit 1 dentro una transazione. La classe rinuncia prima della conferma: annulla. Qual è lo stato finale? Perché non basta eliminare a mano soltanto la prenotazione?
Esercizio 3 — Dodici richiesti, dieci disponibili
Prova a prenotare dodici unità del kit 1 usando un aggiornamento condizionato alla disponibilità. Come riconosci il mancato aggiornamento senza aspettare un errore SQL? Completa il ramo che annulla il tentativo e non inserisce la prenotazione 103.
Esercizio 4 — Un vincolo fallisce dopo due passaggi riusciti
Apri un blocco, inserisci (104, 1, 2) e riduci la disponibilità di due unità. Poi tenta UPDATE scorte SET disponibili = -1 WHERE id = 1;. Intercetta l’errore e annulla l’intero blocco. Quali modifiche devono scomparire? Quale ruolo hanno rispettivamente CHECK e ROLLBACK?
Esercizio 5 — Che cosa resta dopo una chiave duplicata?
Apri una transazione e inserisci (105, 1, 2). Prova poi a inserire (105, 1, 1). Intercetta l’errore senza chiudere subito il blocco: osserva con.in_transaction, prenotazioni e scorte. Infine annulla. L’errore della seconda istruzione aveva già annullato la prima? Questo esercizio osserva uno stato intermedio e non rappresenta una prenotazione completa.
Esercizio 6 — Trova il bug nel programma che conferma comunque
Un programma apre una transazione, inserisce (106, 1, 2), tenta una seconda riga con lo stesso id e ignora l’errore. Poi esegue COMMIT, senza aver mai ridotto le scorte. Prevedi il risultato e spiega perché è sbagliato per la scuola anche se COMMIT riesce. Proponi la regola applicativa corretta; non proporre un ROLLBACK dopo il commit come rimedio.
Soluzioni commentate
Soluzione 1 — Confermare soltanto il percorso riuscito
con.execute("BEGIN")
try:
cur = con.execute(
"UPDATE scorte SET disponibili = disponibili - ? "
"WHERE id = ? AND disponibili >= ?", (3, 1, 3)
)
if cur.rowcount != 1:
raise ValueError("Kit assente o disponibilità insufficiente")
con.execute("INSERT INTO prenotazioni VALUES (?, ?, ?)", (101, 1, 3))
con.execute("COMMIT")
except (sqlite3.Error, ValueError):
if con.in_transaction:
con.execute("ROLLBACK")
raise
Il kit 1 scende a 7; il kit 2 resta a 4; compare soltanto (101, 1, 3). Il controllo avviene prima dell’inserimento. Nel percorso di errore si annulla, se il blocco è ancora attivo, e si rilancia l’eccezione: il chiamante non deve scambiare il fallimento per una prenotazione riuscita. Non è una gestione completa di tutti i guasti possibili, ma rende esplicita la decisione finale.
Soluzione 2 — Annullare entrambe le modifiche
BEGIN;
INSERT INTO prenotazioni VALUES (102, 1, 4);
UPDATE scorte SET disponibili = disponibili - 4 WHERE id = 1;
ROLLBACK;
Tornano le scorte (1, 10) e (2, 4); non resta alcuna prenotazione. Eliminare solo la riga di prenotazione lascerebbe la disponibilità a 6 e richiederebbe un’altra correzione. ROLLBACK annulla insieme le modifiche non confermate del blocco.
Soluzione 3 — Leggere il numero di righe
con.execute("BEGIN")
cur = con.execute(
"UPDATE scorte SET disponibili = disponibili - ? "
"WHERE id = ? AND disponibili >= ?", (12, 1, 12)
)
if cur.rowcount != 1:
con.execute("ROLLBACK")
else:
con.execute("INSERT INTO prenotazioni VALUES (?, ?, ?)", (103, 1, 12))
con.execute("COMMIT")
cur.rowcount vale 0, perché la disponibilità non soddisfa la condizione. Non c’è un errore SQL: SQLite precisa questo comportamento in “UPDATE”, §2, primo paragrafo [4]. Il ramo eseguito è quello di annullamento. Risultato: scorte 10 e 4, nessuna prenotazione. Questo frammento isola il controllo delle righe; per gestire anche altri errori si usa il percorso try/except della soluzione 1.
Soluzione 4 — Il vincolo ferma l’istruzione; il programma annulla il blocco
con.execute("BEGIN")
try:
con.execute("INSERT INTO prenotazioni VALUES (104, 1, 2)")
con.execute("UPDATE scorte SET disponibili = disponibili - 2 WHERE id = 1")
con.execute("UPDATE scorte SET disponibili = -1 WHERE id = 1")
except sqlite3.IntegrityError:
if con.in_transaction:
con.execute("ROLLBACK")
else:
raise AssertionError("Il CHECK avrebbe dovuto rifiutare -1")
Il vincolo impedisce che l’ultima istruzione registri una disponibilità negativa. Nel caso SQLite provato qui le due istruzioni precedenti non vengono automaticamente annullate da quel solo errore. Il ROLLBACK applicativo elimina sia la prenotazione 104 sia la riduzione già eseguita. Risultato finale: scorte 10 e 4, prenotazioni vuote. L’else segnala un esito inatteso del test, non è un ramo operativo da usare per confermare.
Soluzione 5 — La transazione è ancora aperta
con.execute("BEGIN")
con.execute("INSERT INTO prenotazioni VALUES (105, 1, 2)")
try:
con.execute("INSERT INTO prenotazioni VALUES (105, 1, 1)")
except sqlite3.IntegrityError:
print(con.in_transaction)
print(con.execute("SELECT * FROM prenotazioni ORDER BY id").fetchall())
con.execute("ROLLBACK")
Dopo l’errore e prima dell’annullamento, si osservano True e [(105, 1, 2)]. Le scorte sono ancora 10 e 4: in questo esperimento non le abbiamo aggiornate. Il secondo inserimento non sopravvive, ma il primo è presente nel blocco aperto. Dopo ROLLBACK, non resta nessuna prenotazione. È la differenza tra il comportamento SQLite predefinito ABORT e l’annullamento della transazione [3, voce “ABORT”].
Soluzione 6 — Un commit riuscito può fissare dati applicativamente sbagliati
Il frammento seguente è deliberatamente sbagliato: serve a riprodurre e riconoscere il bug, non è un modello da copiare.
con.execute("BEGIN")
con.execute("INSERT INTO prenotazioni VALUES (106, 1, 2)")
try:
con.execute("INSERT INTO prenotazioni VALUES (106, 1, 1)")
except sqlite3.IntegrityError:
pass # BUG: l'errore viene ignorato.
con.execute("COMMIT") # BUG: manca anche la riduzione delle scorte.
Resta una sola prenotazione, (106, 1, 2); le scorte restano 10 e 4. Il database ha rispettato i suoi vincoli dichiarati, ma due kit risultano prenotati senza una corrispondente riduzione delle disponibilità. L’atomicità non impone un comando che il programma non ha mai eseguito.
La correzione è definire tutti i passaggi obbligatori, controllarne l’esito e confermare soltanto se riescono tutti. Per questa prenotazione: validare la quantità, aggiornare una sola riga, registrare una sola prenotazione e confermare; se un passaggio fallisce, annullare il blocco ancora attivo. Un ROLLBACK successivo al commit non elimina la prenotazione già confermata. Il laboratorio mostra il caso errato in un database separato, che viene poi chiuso e scartato.
Fonti per approfondire
[1] PostgreSQL Global Development Group, PostgreSQL 18 Documentation, tutorial §3.4 “Transactions”. Localizzatori: paragrafi iniziali sull’operazione “all-or-nothing”; esempio BEGIN/COMMIT; paragrafo ROLLBACK; nota sulle librerie client; ultimo paragrafo sullo stato dopo un errore. URL: https:/
[2] SQLite Project, SQLite Documentation, “Transaction”, documentazione online a revisione continua, pagina aggiornata il 18 febbraio 2026. Localizzatori: §2 “Transactions”; §2.3 “Implicit versus explicit transactions”; §3 “Response To Errors Within A Transaction”. URL: https:/
[3] SQLite Project, SQLite Documentation, “The ON CONFLICT Clause”, documentazione online a revisione continua. Localizzatori: introduzione sui vincoli interessati; voci “ROLLBACK” e “ABORT”. URL: https:/
Le fonti [2] e [3] appartengono allo stesso progetto; la fonte [1] è di un progetto indipendente. Gli esempi e le attività sono originali e non riproducono i tutorial.
[4] SQLite Project, SQLite Documentation, “UPDATE”, documentazione online a revisione continua. Localizzatore: §2 “Details”, primo paragrafo: una condizione WHERE che non corrisponde ad alcuna riga non è un errore e produce zero righe aggiornate. URL: https:/
[5] Python Software Foundation, Python 3.12 Documentation, modulo sqlite3. Localizzatori: “Transaction control via the isolation_level attribute”; attributi Connection.in_transaction, Connection.autocommit e Cursor.rowcount. URL: https:/
Revisione docente e prova in classe: aperte