Due vincoli che si ripagano subito
La maggior parte dei bug che nascono da uno schema trascurato risale a una di due cose: una colonna che è NULL quando nessuno se lo aspettava, oppure una colonna a cui manca un valore che l'applicazione dava per scontato. NOT NULL e DEFAULT risolvono entrambe, e aggiungerli non costa quasi nulla.
Una colonna è obbligatoria e non ha un valore di riserva. Due ce l'hanno. L'inserimento ha dovuto fornire solo email, e SQLite ha riempito il resto. È tutta la funzionalità in un esempio: il resto della pagina riguarda i casi limite.
NOT NULL significa "rifiuta NULL, senza eccezioni"
NOT NULL fa esattamente quello che dice. Qualsiasi tentativo di mettere NULL nella colonna, sia omettendola da un INSERT senza valore predefinito sia scrivendo NULL in modo esplicito, fallisce:
L'errore dice:
Runtime error: NOT NULL constraint failed: posts.title
Stesso risultato se passi NULL direttamente:
INSERT INTO posts (id, title) VALUES (1, NULL);
-- Errore di runtime: NOT NULL constraint failed: posts.title
Questo è il contratto. Se una colonna è logicamente obbligatoria, contrassegnala come NOT NULL e avrai eliminato un'intera classe di bug: nessun codice applicativo potrà far passare di nascosto un NULL al database.
DEFAULT fornisce un valore quando chi inserisce non lo fa
DEFAULT entra in gioco solo quando un INSERT non cita affatto la colonna. Non salva un NULL esplicito:
Il primo inserimento si affida al valore predefinito. Il secondo lo sovrascrive. Se avessi scritto INSERT INTO tasks (title, status) VALUES ('x', NULL), avresti ottenuto un errore NOT NULL constraint failed: la colonna è stata nominata, quindi il valore predefinito non scatta mai.
È il modello mentale da tenere a mente: DEFAULT sostituisce le colonne mancanti. NOT NULL rifiuta i null in qualunque modo arrivino. Sono funzionalità indipendenti che si combinano bene.
I valori predefiniti possono essere espressioni
Un valore predefinito letterale è il caso comune (DEFAULT 0, DEFAULT '', DEFAULT 'pending'), ma SQLite accetta anche un'espressione tra parentesi. È così che timbri le righe con l'ora in cui sono state create, o generi un ID casuale:
Alcune cose da sapere:
- L'espressione viene valutata a ogni inserimento, non una sola volta alla creazione della tabella. Ogni riga ottiene il proprio timestamp e il proprio token.
CURRENT_TIMESTAMP,CURRENT_DATEeCURRENT_TIMEsono le tre parole chiave speciali che non hanno bisogno di parentesi. Tutto il resto sì.- L'espressione non può fare riferimento ad altre colonne o a subquery: deve essere autosufficiente.
Se vuoi una colonna facoltativa ma timbrata automaticamente, togli NOT NULL e tieni il valore predefinito. Se la vuoi obbligatoria e timbrata automaticamente, usa entrambi.
DEFAULT NULL è lecito (e a volte è proprio il punto)
Scrivere DEFAULT NULL equivale a non avere alcun valore predefinito: la colonna è NULL quando non fornisci un valore. Vale la pena usarlo quando vuoi rendere esplicito nello schema che "nessun valore" è lo stato iniziale previsto:
Qui bio e avatar si comportano in modo identico. Il DEFAULT NULL su bio è un commento sotto forma di codice: dice a chi legge lo schema che l'assenza di una bio è uno stato normale, non una svista.
Aggiungere NOT NULL a una tabella esistente
Qui le cose si complicano. ALTER TABLE in SQLite è volutamente limitato: non puoi eseguire ALTER COLUMN ... SET NOT NULL come faresti in Postgres. Quello che puoi fare dipende dal fatto che la colonna esista già o no.
Per una colonna nuova, ADD COLUMN ... NOT NULL funziona, ma devi fornire un valore predefinito: altrimenti le righe esistenti conterrebbero all'improvviso NULL in una colonna NOT NULL, il che è impossibile:
Prova la stessa cosa senza valore predefinito e otterrai un errore:
ALTER TABLE products ADD COLUMN sku TEXT NOT NULL;
-- Errore di runtime: Cannot add a NOT NULL column with default value NULL
Per una colonna esistente non c'è una modifica sul posto. La ricetta standard è la ricostruzione: crei una nuova tabella con il vincolo che vuoi, copi i dati, elimini la vecchia e rinomini la nuova. La vedremo nella pagina drop-and-alter-table: per ora ti basta sapere che il limite è reale e progettare lo schema tenendone conto.
Una combinazione realistica
La maggior parte delle tabelle in produzione usa entrambi i vincoli insieme per codificare "ciò che l'applicazione si aspetta che sia vero":
Leggi quello schema dall'alto in basso e puoi intuire cosa fa l'applicazione senza vedere una sola riga di codice. customer è obbligatorio e non ha un valore di riserva: chi inserisce deve sapere per chi è l'ordine. Importo, valuta e stato hanno tutti valori predefiniti sensati, quindi anche l'inserimento più semplice produce una riga coerente. notes è facoltativo. created_at viene riempito dal database, che è l'unico posto in cui dovrebbe essere riempito.
Ecco il valore di questi vincoli: trasformano le supposizioni in regole che il database stesso fa rispettare.
Errori comuni
Un breve elenco di cose che mettono in difficoltà:
- Un
NULLesplicito vanificaDEFAULT.INSERT INTO t (col) VALUES (NULL)non usa il valore predefinito. La colonna deve essere assente dall'elenco delle colonne. - Le espressioni predefinite vogliono le parentesi.
DEFAULT CURRENT_TIMESTAMPfunziona (è una delle tre parole chiave speciali).DEFAULT lower(hex(randomblob(8)))no: racchiudila,DEFAULT (lower(hex(randomblob(8)))). NOT NULLe la stringa vuota sono cose diverse.''è un valoreTEXTvalido e non fa scattare il vincolo. Se vuoi vietare anche le stringhe vuote, serve unCHECK(prossima pagina).ADD COLUMN ... NOT NULLrichiede unDEFAULTdiverso daNULL. Senza, SQLite rifiuta la modifica.
Prossimo passo: i vincoli CHECK
NOT NULL e DEFAULT coprono "deve esistere" e "riempi se manca". Per il livello successivo di validazione, come "deve essere positivo", "deve essere uno di questi valori" o "la data di fine deve essere dopo quella di inizio", SQLite ha i vincoli CHECK, che ti permettono di scrivere espressioni booleane arbitrarie che ogni riga deve soddisfare. È il tema della prossima pagina.
Domande frequenti
Come rendo obbligatoria una colonna in SQLite?
Aggiungi NOT NULL alla definizione della colonna: email TEXT NOT NULL. Qualsiasi INSERT o UPDATE che prova a lasciare quella colonna a NULL fallisce con NOT NULL constraint failed. Abbinalo a un DEFAULT se vuoi un valore di riserva quando chi inserisce non ne fornisce uno.
Come funzionano i valori predefiniti in SQLite?
DEFAULT <value> assegna a una colonna un valore da usare quando un INSERT non ne specifica uno. Il valore predefinito può essere un letterale (DEFAULT 0, DEFAULT 'pending'), NULL o un'espressione tra parentesi come DEFAULT (CURRENT_TIMESTAMP) o DEFAULT (lower(hex(randomblob(8)))). Le espressioni predefinite vengono rivalutate a ogni inserimento.
Perché SQLite dice 'NOT NULL constraint failed' quando inserisco?
Stai inserendo una riga senza fornire un valore per una colonna NOT NULL che non ha un DEFAULT. Includi la colonna nel tuo INSERT, dai alla colonna un DEFAULT oppure allenta il vincolo. Anche passare NULL in modo esplicito lo fa scattare: NOT NULL rifiuta i null da qualunque parte arrivino.
Posso aggiungere NOT NULL a una colonna esistente in SQLite?
Non direttamente: in SQLite ALTER TABLE ... ALTER COLUMN non esiste. Puoi aggiungere una nuova colonna con NOT NULL DEFAULT <value> (il valore predefinito è obbligatorio per le righe esistenti), oppure ricostruire la tabella: ne crei una nuova con il vincolo, copi i dati, elimini la vecchia e rinomini la nuova.