Menu

Chiavi esterne in SQLite: REFERENCES, ON DELETE e PRAGMA

Come funzionano le chiavi esterne in SQLite: dichiarare REFERENCES, attivare il controllo con PRAGMA e scegliere il comportamento ON DELETE giusto.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Una chiave esterna è un puntatore tra tabelle

Una chiave esterna (foreign key) è una colonna di una tabella il cui valore deve corrispondere a una riga di un'altra tabella. È il modo in cui i database relazionali dicono "questa riga di posts appartiene a quella riga di authors" senza copiare il nome e l'email dell'autore in ogni post.

Ecco l'esempio più piccolo possibile: una tabella genitore e una tabella figlia collegate da una FK:

author_id INTEGER REFERENCES authors(id) è tutta la dichiarazione della chiave esterna. Dice: questa colonna contiene un id della tabella authors. Ora il database sa che le due tabelle sono collegate e, se il controllo è attivo, rifiuterà gli inserimenti che puntano ad autori inesistenti.

Le chiavi esterne sono disattivate di default

È il fatto più importante sulle chiavi esterne in SQLite, e sorprende tutti: SQLite legge le clausole REFERENCES ma non le fa rispettare a meno che tu non lo chieda. Il motivo è la compatibilità storica: i database più vecchi sono stati creati prima che la funzionalità esistesse.

Guarda cosa succede senza il controllo:

La riga orfana è entrata senza ostacoli. Per avere la protezione che ti serve davvero, esegui PRAGMA foreign_keys = ON; all'inizio di ogni connessione:

Ora l'inserimento fallisce con FOREIGN KEY constraint failed. Il pragma vale per connessione, non per database: l'impostazione non viene salvata nel file. Ogni applicazione, ogni sessione della CLI, ogni fixture di test deve impostarlo. La maggior parte del codice in produzione esegue PRAGMA foreign_keys = ON; subito dopo aver aperto una connessione.

Cosa richiede davvero la clausola REFERENCES

La colonna che referenzi deve essere una PRIMARY KEY o avere un vincolo UNIQUE. È così che SQLite può garantire che la ricerca non sia ambigua. Anche i tipi dovrebbero essere compatibili: SQLite è permissivo sui tipi, ma mescolarli vuol dire andare in cerca di sorprese.

Puoi scrivere la FK in due modi. In linea sulla colonna:

Oppure come vincolo separato a livello di tabella, obbligatorio quando la chiave esterna comprende più colonne:

Le due forme producono vincoli identici. Usa quella che si legge meglio per la tabella che stai scrivendo.

ON DELETE: cosa succede ai figli

Quando elimini una riga genitore, SQLite deve decidere cosa fare con i figli che puntano a essa. La regola la scegli con ON DELETE:

Eliminando Ada sono stati eliminati anche entrambi i suoi post. Le opzioni sono:

  • CASCADE: elimina anche i figli. Va bene per i dati "posseduti", come i post di un autore o le voci di un ordine.
  • SET NULL: mette a NULL la colonna della FK. Va bene quando i figli devono sopravvivere senza genitore (per esempio, i commenti di un utente eliminato diventano anonimi).
  • SET DEFAULT: imposta la colonna della FK sul suo valore predefinito dichiarato.
  • RESTRICT: blocca l'eliminazione se esistono figli. Fallisce subito, al momento dell'istruzione.
  • NO ACTION: il predefinito. Nella maggior parte dei casi si comporta come RESTRICT (rimanda il controllo al commit, ma il risultato è lo stesso: non puoi lasciare figli orfani).

ON UPDATE funziona allo stesso modo per le modifiche alla chiave del genitore, anche se aggiornare le chiavi primarie è raro.

Foreign key constraint failed: cosa significa

Vedrai questo errore in due situazioni. La prima: inserisci o aggiorni un figlio con un valore che non ha un genitore corrispondente:

sqlite> INSERT INTO posts (title, author_id) VALUES ('Stray', 999);
Runtime error: FOREIGN KEY constraint failed

O l'autore 999 non esiste, o hai confuso i tipi delle colonne. Inserisci prima il genitore, oppure correggi il valore.

La seconda: elimini (o aggiorni) un genitore che ha ancora dei figli, quando la FK usa RESTRICT o NO ACTION:

sqlite> DELETE FROM authors WHERE id = 1;
Runtime error: FOREIGN KEY constraint failed

Elimina prima i figli, oppure cambia la FK in ON DELETE CASCADE/SET NULL se è la cascata che vuoi davvero.

C'è anche un parente meno comune, FOREIGN KEY mismatch. Scatta quando la colonna referenziata non è una chiave primaria o univoca, oppure quando il numero di colonne non coincide. È un errore di schema, non di dati.

Aggiungere chiavi esterne a tabelle esistenti

ALTER TABLE in SQLite è limitato: puoi aggiungere una colonna con una chiave esterna, ma non puoi applicare una chiave esterna a una colonna che esiste già. La soluzione standard è ricostruire la tabella e rinominarla:

Lo schema: disattiva il controllo, crea la nuova tabella con i vincoli che vuoi, copia i dati, elimina la vecchia tabella, rinomina. BEGIN/COMMIT rendono il tutto atomico. Riattiva il controllo alla fine e SQLite verificherà le righe esistenti rispetto ai nuovi vincoli: se ci sono dati non validi, però, la transazione è già stata confermata, quindi controlla prima se hai dubbi.

Esegui PRAGMA foreign_key_check; dopo la migrazione per confermare che non ci siano righe orfane.

Uno schema realistico

Mettiamo tutto insieme: un piccolo schema per un blog, con genitori, figli e una tabella di collegamento per i tag molti a molti:

Tre cose da notare. author_id è NOT NULL: ogni post deve avere un autore. La FK posts → authors va in cascata, quindi eliminando un autore si cancellano i suoi post. La tabella di collegamento post_tags va in cascata da entrambi i lati, quindi rimuovendo un post o un tag le righe di collegamento vengono ripulite automaticamente.

Abitudini che ti evitano guai

  • Imposta PRAGMA foreign_keys = ON; su ogni connessione. Rendilo parte della routine con cui la tua applicazione apre il database, non qualcosa da ricordare.
  • Aggiungi un indice sulla colonna della FK. SQLite indicizza automaticamente la chiave del genitore, ma non quella del figlio, e ON DELETE CASCADE fa una ricerca sul figlio ogni volta che viene eliminato il genitore.
  • Scegli ON DELETE in modo consapevole. Il predefinito (NO ACTION) è sicuro, ma significa che ti scontrerai con "constraint failed" ogni volta che provi a fare pulizia. Decidi cosa deve succedere e dichiaralo.
  • Esegui PRAGMA foreign_key_check; dopo migrazioni o importazioni massive per scovare gli orfani prima che diventino bug.

Prossimo passo: INNER JOIN

Le chiavi esterne descrivono la relazione; le join sono il modo in cui interroghi i dati attraverso di essa. La prossima pagina tratta INNER JOIN: combinare righe di tabelle collegate e ottenere le colonne che ti servono da ciascuna.

Domande frequenti

Come creo una chiave esterna in SQLite?

Aggiungi una clausola REFERENCES other_table(column) alla definizione della colonna in CREATE TABLE. Per esempio, author_id INTEGER REFERENCES authors(id) fa puntare author_id a una riga di authors. La colonna referenziata deve essere una PRIMARY KEY o avere un vincolo UNIQUE.

Perché in SQLite le chiavi esterne non vengono applicate?

SQLite legge le dichiarazioni di chiave esterna ma non le fa rispettare finché non attivi il controllo. Esegui PRAGMA foreign_keys = ON; all'inizio di ogni connessione. L'impostazione vale per connessione e non viene salvata nel database, quindi librerie e CLI devono impostarla a ogni collegamento.

Cosa fa ON DELETE CASCADE in SQLite?

ON DELETE CASCADE dice a SQLite di eliminare automaticamente le righe figlie quando viene eliminata la riga genitore. Le altre opzioni sono RESTRICT (blocca l'eliminazione), SET NULL (mette a NULL la colonna della FK), SET DEFAULT e NO ACTION (il predefinito, in pratica uguale a RESTRICT). Scegli in base al fatto che le righe figlie abbiano senso anche senza il genitore.

Come risolvo 'foreign key constraint failed' in SQLite?

L'errore significa che hai provato a inserire o aggiornare una riga il cui valore di chiave esterna non corrisponde a nessuna riga della tabella referenziata, oppure che hai provato a eliminare un genitore che ha ancora dei figli. Verifica prima che la riga referenziata esista, oppure imposta ON DELETE CASCADE se vuoi che i figli vengano rimossi automaticamente.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA