Descrizione
L'istruzione ALTER TABLE ADD CONSTRAINT FOREIGN KEY è il pilastro dell'integrità referenziale in un database. Una chiave esterna crea un legame "parentela" tra due tabelle (padre e figlia), garantendo che non possano esistere riferimenti a dati inesistenti. Trasforma un semplice archivio di dati in un sistema coerente dove le relazioni sono garantite dal motore del database.
La tabella figlia contiene la chiave esterna che punta alla chiave primaria (o unica) della tabella padre. È fondamentale che i tipi di dati delle due colonne siano identici (es. entrambi INT UNSIGNED). Il motore di storage InnoDB è obbligatorio per far funzionare queste restrizioni in MySQL/MariaDB.
La potenza delle Foreign Key risiede nelle clausole ON DELETE e ON UPDATE. Esse determinano il "fato" dei dati figli quando il padre viene modificato o cancellato: scegliere CASCADE, SET NULL o RESTRICT ha implicazioni drastiche sulla logica di business e sulla sicurezza dei dati.
Sintassi
ALTER TABLE tabella_figlia
ADD CONSTRAINT nome_fk
FOREIGN KEY (colonna_figlia)
REFERENCES tabella_padre (colonna_padre)
ON DELETE [AZIONE]
ON UPDATE [AZIONE];
Dove [AZIONE] può essere: CASCADE, SET NULL, RESTRICT, NO ACTION.
Esempio 1:ON DELETE CASCADE (Eliminazione a cascata)
ALTER TABLE ordini
ADD CONSTRAINT fk_ordini_clienti
FOREIGN KEY (cliente_id)
REFERENCES clienti (id)
ON DELETE CASCADE
ON UPDATE CASCADE;
Logica: L'opzione CASCADE rende il database un "domino". Se elimini un cliente (padre), il database elimina automaticamente tutti i suoi ordini (figli). Non puoi avere un ordine senza cliente.
Stato Iniziale
Tabella: clienti
| id |
nome |
| 1 |
Mario Rossi |
| 2 |
Laura Bianchi |
| 3 |
Giuseppe Verdi |
Tabella: ordini
| id |
cliente_id |
importo |
| 100 |
1 |
150.00 |
| 101 |
2 |
230.00 |
| 102 |
3 |
95.00 |
| 103 |
3 |
40.00 |
DELETE FROM clienti WHERE id = 3;
➜
Dopo (ON DELETE CASCADE)
Tabella: clienti
| id |
nome |
| 1 |
Mario Rossi |
| 2 |
Laura Bianchi |
Tabella: ordini
| id |
cliente_id |
importo |
| 100 |
1 |
150.00 |
| 101 |
2 |
230.00 |
✅ Cliente 3 eliminato → Ordini 102 e 103 eliminati automaticamente!
Esempio 2: ON DELETE SET NULL
ALTER TABLE dipendenti
ADD CONSTRAINT fk_dipendenti_reparti
FOREIGN KEY (reparto_id)
REFERENCES reparti (id)
ON DELETE SET NULL
ON UPDATE CASCADE;
Logica: Qui usiamo SET NULL. Se un reparto viene chiuso (padre eliminato), non vogliamo licenziare i dipendenti (figli). Vogliamo solo che smettano di appartenere a quel reparto. Il campo reparto_id diventa NULL. Nota: La colonna nella tabella figlia deve accettare NULL.
Prima (Reparto Marketing esiste)
Tabella: dipendenti
| id |
nome |
reparto_id |
| 1 |
Mario Rossi |
1 |
| 2 |
Giulia Verdi |
2 |
➜
Dopo (ON DELETE SET NULL)
Tabella: dipendenti
| id |
nome |
reparto_id |
| 1 |
Mario Rossi |
1 |
| 2 |
Giulia Verdi |
NULL |
✅ Reparto marketing eliminato, ma Giulia è ancora in azienda (senza assegnazione).
Esempio 3: ON DELETE RESTRICT
ALTER TABLE prodotti
ADD CONSTRAINT fk_prodotti_categorie
FOREIGN KEY (categoria_id)
REFERENCES categorie (id)
ON DELETE RESTRICT
ON UPDATE NO ACTION;
Logica: RESTRICT (o l'equivalente predefinito) è il "Guardiano". Se provi a eliminare una categoria che contiene ancora prodotti, il database blocca l'operazione con un errore. Ti obbliga a gestire manualmente i prodotti prima di chiudere la categoria. È l'opzione più sicura per evitare cancellazioni accidentali.
Tentativo di eliminare categoria
Tabella: categorie
| id |
nome |
| 1 |
Informatica |
| 2 |
Libri (Attiva) |
Tabella: prodotti
| id |
nome |
categoria_id |
| 10 |
Monitor |
1 |
| 11 |
D&D Manuale |
2 |
| 12 |
Harry Potter |
2 |
❌ Errore: Cannot delete or update a parent row: a foreign key constraint fails.
Non puoi eliminare la categoria "Libri" perché ci sono prodotti (ID 11, 12) collegati.
Esempio 4: Foreign Key Auto-referenziale (Gerarchie)
ALTER TABLE dipendenti
ADD CONSTRAINT fk_dipendenti_manager
FOREIGN KEY (manager_id)
REFERENCES dipendenti (id)
ON DELETE SET NULL;
Logica: Una tabella può referenziare se stessa. È utile per strutture ad albero (organigrammi). Un dipendente è un record nella tabella, ma il suo manager_id punta all'id di un altro record nella stessa tabella.
Visualizzazione Organigramma
| id (PK) |
nome dipendente |
manager_id (FK -> id) |
Note |
| 10 |
CEO Azienda |
NULL |
Nessun manager (è il capo) |
| 20 |
Resp. IT |
10 |
Il suo manager è il CEO (id 10) |
| 21 |
Sviluppatore Senior |
20 |
Il suo manager è il Resp. IT (id 20) |
| 22 |
Stagista |
21 |
Il suo manager è lo Sviluppatore (id 21) |
✅ Se cancelliamo lo Sviluppatore Senior (id=21), lo stagista (id=22) non viene cancellato, ma il suo manager_id diventa NULL (perde il riferimento al capo).
Esempio 5: Aggiornamento a cascata (ON UPDATE CASCADE)
Caso d'uso: Utilizzo di chiavi non numeriche (es. codici alfanumerici) che possono essere rinominati. Se cambia il codice del "padre", deve cambiare ovunque sia stato usato.
ALTER TABLE libri
ADD CONSTRAINT fk_libri_generi
FOREIGN KEY (codice_genere)
REFERENCES generi (codice)
ON UPDATE CASCADE;
Scenario: La tabella generi usa codici brevi (es. 'THR' per Thriller). Decidiamo di cambiare il codice in una forma più estesa ('THRI'). Senza CASCADE, dovremmo aggiornare manualmente migliaia di libri. Con CASCADE, il database propaga la modifica.
Stato Attuale
Tabella: generi (Padre)
| codice (PK) |
nome |
| THR |
Thriller |
| SF |
Sci-Fi |
Tabella: libri (Figlia)
| titolo |
codice_genere (FK) |
| Il silenzio degli innocenti |
THR |
| Shining |
THR |
UPDATE generi SET codice = 'THRI' WHERE codice = 'THR';
➜
Dopo l'UPDATE
Tabella: generi
| codice |
nome |
| THRI |
Thriller |
| SF |
Sci-Fi |
Tabella: libri
| titolo |
codice_genere |
| Il silenzio degli innocenti |
THRI |
| Shining |
THRI |
✅ La modifica del codice nella tabella padre si è propagata automaticamente a tutti i libri collegati!
Esempio 6: Aggiornamento con disassociazione (ON UPDATE SET NULL)
Caso d'uso: Se l'identificativo del padre cambia, non vogliamo che i figli seguano il nuovo identificativo, ma preferiamo "sganciarli" (NULL), magari per richiedere una riassegnazione manuale.
ALTER TABLE dipendenti
ADD CONSTRAINT_fk_dip_progetti
FOREIGN KEY (codice_progetto)
REFERENCES progetti (codice)
ON UPDATE SET NULL;
Scenario: Un progetto viene rinominato da 'PRJ_ALPHA' a 'PRJ_BETA'. Per policy aziendale, se un progetto cambia codice fondamentale, i dipendenti assegnati al vecchio codice vengono "sganciati" (NULL) affinché un manager possa decidere se riassegnarli al nuovo codice 'PRJ_BETA' manualmente o ad altro.
Prima dell'aggiornamento
Tabella: progetti (Padre)
| codice |
stato |
| PRJ_OLD |
Attivo |
Tabella: dipendenti (Figlia)
| nome |
codice_progetto (FK) |
| Mario Rossi |
PRJ_OLD |
| Luigi Verdi |
PRJ_OLD |
UPDATE progetti SET codice = 'PRJ_NEW' WHERE codice = 'PRJ_OLD';
➜
Dopo l'UPDATE
Tabella: progetti
| codice |
stato |
| PRJ_NEW |
Attivo |
Tabella: dipendenti
| nome |
codice_progetto |
| Mario Rossi |
NULL |
| Luigi Verdi |
NULL |
⚠️ Il codice progetto è cambiato, ma i dipendenti sono stati disassociati (NULL). Devono essere riassegnati manualmente.
Esempio 7: Aggiornamento e cancellazione a cascata (ON UPDATE CASCADE + ON DELETE CASCADE)
Caso d'uso: Gestione di codici categoria dinamici. Quando un codice categoria viene modificato, l'aggiornamento si propaga automaticamente. Quando una categoria viene eliminata, tutti gli elementi associati vengono rimossi in sicurezza.
ALTER TABLE prodotti
ADD CONSTRAINT fk_prodotti_categorie
FOREIGN KEY (codice_categoria)
REFERENCES categorie (codice)
ON UPDATE CASCADE
ON DELETE CASCADE;
Scenario:
ON UPDATE CASCADE: Se il codice categoria 'ELE' viene rinominato in 'ELET', tutti i prodotti aggiornano automaticamente il riferimento.
ON DELETE CASCADE: Se la categoria 'ABG' (Abbigliamento) viene eliminata, tutti i prodotti associati vengono cancellati automaticamente per mantenere l'integrità referenziale.
Nota: Entrambe le clausole sono opzionali e vanno definite esplicitamente quando necessarie.
Stato Iniziale (Prima dell'UPDATE)
Tabella: categorie (Padre)
| codice (PK) | nome |
| ELE | Elettronica |
| ABG | Abbigliamento |
Tabella: prodotti (Figlia)
| prodotto | codice_categoria (FK) |
| Smartphone X | ELE |
| Cuffie Pro | ELE |
| Maglione Inverno | ABG |
UPDATE categorie SET codice = 'ELET' WHERE codice = 'ELE';
➜
Dopo l'UPDATE
Tabella: categorie
| codice | nome |
| ELET | Elettronica |
| ABG | Abbigliamento |
Tabella: prodotti
| prodotto | codice_categoria |
| Smartphone X | ELET |
| Cuffie Pro | ELET |
| Maglione Inverno | ABG |
✅ L'aggiornamento del codice categoria si è propagato automaticamente a tutti i prodotti!
----------
Stato Iniziale (Prima della DELETE)
Tabella: categorie (Padre)
| codice (PK) | nome |
| ELET | Elettronica |
| ABG | Abbigliamento |
Tabella: prodotti (Figlia)
| prodotto | codice_categoria (FK) |
| Smartphone X | ELET |
| Cuffie Pro | ELET |
| Maglione Inverno | ABG |
| Sciarpa di Lana | ABG |
DELETE FROM categorie WHERE codice = 'ABG';
➜
Dopo la DELETE
Tabella: categorie
| codice | nome |
| ELET | Elettronica |
Tabella: prodotti
| prodotto | codice_categoria |
| Smartphone X | ELET |
| Cuffie Pro | ELET |
✅ L'eliminazione della categoria ha rimosso automaticamente tutti i prodotti associati!
Esempio 8: Chiave Esterna Composta (Composite Key)
ALTER TABLE movimenti_magazzino
ADD CONSTRAINT fk_movimenti_inventario
FOREIGN KEY (prodotto_id, magazzino_id)
REFERENCES inventario (prodotto_id, magazzino_id)
ON DELETE RESTRICT;
Logica: In questo caso la chiave primaria della tabella padre (inventario) è formata da due colonne. La foreign key deve riferirsi a entrambe contemporaneamente. Questo garantisce che un movimento sia valido solo per quella specifica combinazione di Prodotto e Magazzino.
Tabella Padre: inventario (PK Composta)
| prodotto_id (PK) |
magazzino_id (PK) |
quantità |
| 101 |
A |
50 |
| 101 |
B |
20 |
Lo stesso prodotto (101) esiste in due magazzini diversi.
➜
Tabella Figlia: movimenti_magazzino (FK Composta)
| id_mov |
prodotto_id (FK) |
magazzino_id (FK) |
tipo |
| 900 |
101 |
A |
Entrata |
| 901 |
101 |
B |
Uscita |
| 902 |
101 |
C |
Uscita |
❌ Errore nel movimento 902: La coppia (101, C) non esiste nella tabella inventario!
Esempio 9: Relazione Uno-a-Uno (One-to-One)
ALTER TABLE profili_utenti
ADD CONSTRAINT fk_profili_utenti
FOREIGN KEY (utente_id)
REFERENCES utenti (id)
ON DELETE CASCADE;
ALTER TABLE profili_utenti
ADD UNIQUE (utente_id);
Logica: A differenza della classica relazione 1-a-Molti, qui ogni riga della tabella figlia è legata a una sola riga padre e viceversa. Si usa tipicamente per separare dati sensibili (es. carta di credito, bio estesa) dalla tabella principale degli utenti. La chiave è che la FK nella tabella figlia ha un vincolo UNIQUE.
| Tabella Padre: utenti (Login) |
| id |
email |
password |
| 1 |
mario@email.com |
****** |
| 2 |
luigi@email.com |
****** |
⬇️ (Relazione UNICA)
| Tabella Figlia: profili_utenti (Dettagli) |
| id |
utente_id (FK, UNIQUE) |
indirizzo_spedizione |
| 50 |
1 |
Via Roma 1 |
| 51 |
2 |
Via Milano 2 |
✅ Ogni utente ha al massimo un profilo. Non puoi creare due profili per l'utente 1 grazie al vincolo UNIQUE.
Opzioni ON DELETE e ON UPDATE a confronto
| Azione |
Comportamento |
Quando usarlo |
CASCADE |
PROPAGA ELIMINAZIONE/MODIFICA AI FIGLI: Con l'eliminazione(l'aggiornamento) di una riga del padre si ha l'eliminazione (l'aggiornamento) delle righe dei figli collegate |
Relazioni forte dipendenza (Ordine effettuato /Righe Ordine effettuato). |
SET NULL |
IMPOSTA NULL NEL FIGLIO:Con l'eliminazione(l'aggiornamento) di una riga del padre si ha l'impostazione a null nelle righe dei figli collegate |
Dati storici o opzionali (Dipendenti/Reparto). |
RESTRICT |
BLOCCA L'OPERAZIONE SUL PADRE: Se cancello una riga del padre che è collegata a righe dell'entità figlia, la cancellazione è bloccata |
Sicurezza massima (Categorie/Prodotti). |
NO ACTION |
SIMILE A RESTRICT (verifica in differita). |
Compatibilità SQL standard, logiche complesse. |
Convenzioni per il nome dei constraint
Un buon nome per la foreign key facilita il debugging e la manutenzione:
- Sintassi consigliata:
fk_[tabella_figlia]_[tabella_padre]_[colonna_opzionale]
- Esempio chiaro:
fk_ordini_clienti dice subito che è il legame tra ordini e clienti.
- Evitare: Nomi generici come
fk1, fk_clienti (ambiguo). Usa sempre il prefisso fk_.
Gestione degli errori comuni
Creare una Foreign Key a volte fallisce. Ecco come risolvere i problemi più frequenti:
- Errore 1215 (Cannot add foreign key): Solitamente causa da tipi di dati diversi (es. padre
INT, figlio BIGINT) o mancato supporto InnoDB. Verifica che charset e collation siano identici.
- Errore 1452 (Cannot add or update a child row): Ci sono già dati "orfani" nella tabella figlia. Prima di creare il vincolo, pulisci i dati:
SELECT figlio.*
FROM tabella_figlio figlio
LEFT JOIN tabella_padre padre ON figlio.colonna_fk = padre.id
WHERE padre.id IS NULL AND figlio.colonna_fk IS NOT NULL;
Verifica e rimozione dei constraint
Per identificare il nome esatto del constraint (che spesso viene generato automaticamente se non specificato):
SHOW CREATE TABLE nome_tabella;
Per rimuovere il vincolo:
ALTER TABLE nome_tabella
DROP FOREIGN KEY nome_del_constraint;
Definizione dei vincoli al momento della creazione delle tabelle
I vincoli (di integrità referenziale, di dominio o di tupla) possono – e spesso è buona pratica – essere definiti direttamente durante la creazione della tabella con CREATE TABLE. Questo rende lo schema più chiaro fin dall’inizio e garantisce coerenza sin dal primo inserimento di dati.
Esempi:
CREATE TABLE prodotti (
id INT PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
prezzo DECIMAL(10,2) CHECK (prezzo > 0)
);
Spiegazione: Il vincolo CHECK (prezzo > 0) è un esempio di vincolo di dominio: impone che il valore del campo prezzo sia sempre maggiore di zero. MySQL lo accetta nella sintassi (anche se in alcune versioni non lo applica rigorosamente a meno che non si usi il motore InnoDB con SQL mode abilitato), ma altri DBMS come PostgreSQL o SQL Server lo applicano in modo rigoroso. È comunque una buona pratica dichiararlo per chiarezza semantica.
CREATE TABLE contatti (
id INT PRIMARY KEY,
email VARCHAR(100),
telefono VARCHAR(20),
CHECK (email IS NOT NULL OR telefono IS NOT NULL)
);
Spiegazione: Questo è un vincolo di tupla perché coinvolge più attributi della stessa riga. La condizione assicura che ogni record abbia almeno un mezzo di contatto: o l’email, o il telefono (o entrambi). Senza questo vincolo, si potrebbero inserire contatti completamente inutilizzabili.
CREATE TABLE ordini (
id INT PRIMARY KEY,
cliente_id INT NOT NULL,
data_ordine DATE NOT NULL,
FOREIGN KEY fk_ordini_clienti (cliente_id)
REFERENCES clienti(id)
ON DELETE CASCADE
);
Spiegazione: Qui viene definito un vincolo di integrità referenziale tramite una foreign key. Il campo cliente_id deve corrispondere a un valore esistente nella colonna id della tabella clienti. L’opzione ON DELETE CASCADE specifica che, se un cliente viene eliminato, tutti i suoi ordini verranno cancellati automaticamente. Il nome del vincolo (fk_ordini_clienti) segue la convenzione suggerita, rendendo facile identificarne il ruolo durante la manutenzione.
Definire i vincoli in fase di creazione evita errori successivi e documenta esplicitamente le regole del modello dati.