Un vincolo CHECK è una regola che ogni riga deve rispettare
Un vincolo CHECK è un'espressione booleana che associ a una tabella. SQLite la valuta a ogni INSERT e UPDATE, e se l'espressione risulta falsa l'operazione fallisce. È un modo per incorporare una regola di business, "il prezzo non può essere negativo", "lo stato deve essere uno di questi tre valori", direttamente nello schema.
Le prime due righe vengono inserite. La terza solleva CHECK constraint failed e viene rifiutata: la tabella non la vede mai. Il vincolo impone la regola a chiunque scriva, che sia la tua app, uno script di migrazione o qualcuno che sta curiosando nella CLI.
A livello di colonna contro a livello di tabella
Puoi scrivere un CHECK in due punti: dopo la definizione di una colonna (a livello di colonna) o dopo tutte le colonne (a livello di tabella). Si comportano allo stesso modo; la differenza è in cosa si legge in modo più naturale.
La prima prenotazione viene inserita. La seconda fallisce: la fine è prima dell'inizio. Le regole su una sola colonna si leggono meglio a livello di colonna; tutto ciò che confronta due o più colonne si legge meglio a livello di tabella.
Limitare i valori a un elenco
Un uso comune è costringere una colonna a uno di un insieme fisso di valori. SQLite non ha un tipo enum nativo, quindi l'idioma è CHECK ... IN (...):
La terza riga fallisce: 'pending' non è nell'elenco dei valori ammessi. Se un giorno dovrai aggiungere un nuovo stato, dovrai ricostruire la tabella (ne parliamo più sotto), quindi pensaci un attimo prima di bloccare l'elenco. Ma per vocabolari davvero fissi, come nomi di ruoli o stati degli ordini, è esattamente il vincolo che ti serve.
Dare un nome ai vincoli
Di default un vincolo è anonimo. Il messaggio di errore dice solo "CHECK constraint failed" con l'espressione, che va bene quando sulla tabella c'è un solo CHECK, ma confonde quando ce ne sono cinque. Aggiungi un nome con CONSTRAINT:
Ora il messaggio di errore include il nome del vincolo, così sai subito quale regola è stata violata. Dare un nome costa qualche carattere in più e si ripaga la prima volta che qualcosa si rompe in produzione.
CHECK e NULL: l'insidia
CHECK passa quando l'espressione è vera oppure NULL. Fallisce solo con un falso esplicito. Sembra strano, finché non ricordi che quasi ogni confronto con NULL restituisce NULL, non vero o falso.
La riga con NULL viene inserita senza problemi: NULL >= 0 è NULL, non falso, quindi il CHECK non fallisce. Se vuoi davvero vietare sia i numeri negativi sia i valori mancanti, combina NOT NULL con il CHECK:
Ora l'insert fallisce sul vincolo NOT NULL prima ancora che venga eseguito il CHECK. I due vincoli lavorano insieme: NOT NULL copre l'assenza, CHECK copre la forma.
Funzioni integrate utili dentro CHECK
L'espressione può usare la maggior parte delle funzioni integrate di SQLite. Alcune capitano spesso:
Tre fallimenti: un'email con una forma sbagliata, uno username troppo corto e un codice paese in minuscolo. LIKE gestisce i pattern semplici; length(), upper(), lower() e l'aritmetica sono tutti ammessi. Tieni però l'espressione deterministica: usare qualcosa come random() o current_timestamp dentro un CHECK crea regole che possono cambiare da una riga all'altra, e raramente è quello che vuoi.
CHECK contro trigger
Sia CHECK sia i trigger possono rifiutare dati sbagliati, e chi inizia si chiede spesso quale usare. La regola pratica:
- CHECK quando la regola dipende solo dalla riga che stai scrivendo. "Questa colonna confrontata con quella", "questo valore dentro un intervallo", "questa stringa corrisponde a un pattern".
- Trigger (nello specifico un trigger
BEFORE INSERT/UPDATEche chiamaRAISE) quando la regola dipende da altre righe, da altre tabelle o deve fare qualcosa di più complesso di una singola espressione booleana.
CHECK è più veloce, più semplice e visibile nello schema: chiunque legga il CREATE TABLE vede la regola. Ricorri a un trigger solo quando CHECK non riesce a esprimere ciò che ti serve.
Non puoi eliminare un CHECK con ALTER
Questo è l'unico punto spigoloso. SQLite non ha ALTER TABLE ... DROP CONSTRAINT. Per rimuovere o cambiare un CHECK, ricostruisci la tabella:
BEGIN;
CREATE TABLE products_new (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);
INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;
COMMIT;
Racchiudi tutto in una transazione, così un errore a metà lascia il database com'era. Se altre tabelle hanno chiavi esterne che puntano alla tabella che stai ricostruendo, il procedimento si allunga: disattivi foreign_keys, ricostruisci, riattivi, ricontrolli. Lo vedremo nella guida sulle migrazioni più avanti nel percorso.
Prossimo passo: vincoli UNIQUE
CHECK valida la forma dei valori dentro una riga. Il prossimo vincolo, UNIQUE, valida le relazioni tra righe: garantisce che due righe non abbiano lo stesso valore in una colonna o in un insieme di colonne. È il prossimo argomento.
Domande frequenti
Cos'è un vincolo CHECK in SQLite?
Un vincolo CHECK è un'espressione booleana associata a una tabella che ogni riga deve soddisfare. SQLite la valuta a ogni INSERT o UPDATE e rifiuta la modifica se l'espressione è falsa. È il modo più semplice per imporre una regola come 'il prezzo deve essere positivo' senza scrivere codice applicativo.
Un vincolo CHECK in SQLite può fare riferimento a più colonne?
Sì: scrivilo come vincolo a livello di tabella invece di associarlo a una sola colonna. Per esempio, CHECK (start_date <= end_date) dichiarato dopo l'elenco delle colonne può fare riferimento a entrambe. Tecnicamente anche i controlli a livello di colonna possono citare altre colonne, ma la forma a livello di tabella si legge meglio quando le colonne coinvolte sono più di una.
Perché il mio vincolo CHECK in SQLite non scatta con NULL?
CHECK passa quando l'espressione è vera oppure NULL: fallisce solo quando l'espressione è esplicitamente falsa. Quindi CHECK (age >= 0) accetta un'età NULL, perché NULL >= 0 è NULL, non falso. Se vuoi vietare anche NULL, aggiungi un vincolo NOT NULL insieme al CHECK.
Posso eliminare o modificare un vincolo CHECK in SQLite?
Non direttamente. SQLite non supporta ALTER TABLE ... DROP CONSTRAINT. Per cambiare un CHECK puoi modificare sqlite_schema con PRAGMA writable_schema (avanzato e rischioso) oppure ricostruire la tabella: crei una nuova tabella con i vincoli desiderati, copi i dati, elimini la vecchia tabella e rinomini la nuova. Dare un nome ai vincoli rende più leggibile lo script di ricostruzione.