Il binding è il modo in cui i valori entrano in un prepared statement
Un prepared statement è SQL con dei buchi. Il binding è l'atto di riempire quei buchi con dei valori, in sicurezza, uno alla volta, attraverso l'API del driver invece di incollare stringhe tra loro.
La struttura è sempre la stessa: scrivi l'SQL con i placeholder, poi passi i valori separatamente.
Nella CLI non puoi davvero mostrare il binding (la shell non ha codice applicativo collegato), ma l'SQL qui sopra è esattamente quello che invia la tua applicazione. I segni ? sono placeholder. Il tuo driver, sqlite3 in Python, better-sqlite3 in Node, rusqlite in Rust, li riempie con una chiamata bind separata.
Il modello mentale: l'SQL è la ricetta, i valori associati sono gli ingredienti. Non si toccano mai.
Placeholder posizionali: ?
Il placeholder più semplice è ?. Ognuno corrisponde al prossimo valore che associ, in ordine.
INSERT INTO users (name, email) VALUES (?, ?);
In Python diventa:
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Rosa", "rosa@example.com"),
)
Il primo ? riceve "Rosa", il secondo "rosa@example.com". Se passi troppi o troppo pochi valori, il driver solleva un errore prima che l'istruzione venga eseguita.
Puoi anche numerarli in modo esplicito con ?1, ?2, ?3: utile quando lo stesso valore compare più di una volta:
SELECT ?1 AS greeting, ?1 AS still_the_same;
?1 riutilizza il primo valore associato. Senza numerazione, dovresti associare lo stesso valore due volte.
Placeholder con nome: :name
Quando un'istruzione ha più di due o tre buchi, il binding posizionale diventa un gioco a indovinare. I parametri con nome risolvono il problema:
INSERT INTO users (name, email)
VALUES (:name, :email);
In Python:
cursor.execute(
"INSERT INTO users (name, email) VALUES (:name, :email)",
{"name": "Boris", "email": "boris@example.com"},
)
L'ordine delle chiavi nel dizionario non conta: contano solo i nomi. SQLite accetta anche @name e $name come prefissi alternativi; si comportano tutti allo stesso modo. :name è di gran lunga il più comune.
I parametri con nome ripagano non appena hai un UPDATE con cinque colonne, o una query che usa lo stesso valore in WHERE e in RETURNING.
Binding di NULL
Il modo giusto per inserire NULL è passare il valore null del tuo linguaggio attraverso l'API di binding. Il driver si occupa della traduzione:
INSERT INTO users (name, email) VALUES (?, ?);
-- Bind: ("Cyrus", None) in Python
-- Bind: ["Cyrus", null] in Node
SELECT id, name, email FROM users;
None, null, nil, comunque lo chiami il tuo linguaggio: il driver lo trasforma in un vero NULL SQL. Non associare la stringa "NULL", che salverebbe il testo di quattro caratteri "NULL". E non inserire la parola NULL nel testo SQL: vanificheresti del tutto il binding.
La stessa regola vale per numeri, blob e date: passa il valore nativo e lascia che il driver lo associ.
Riutilizzare un'istruzione con valori diversi
Il binding va di pari passo con i prepared statement. Prepari una volta, associ ed esegui molte volte. Il parser fa il suo lavoro una sola volta e il database riutilizza il piano compilato per ogni insieme di valori associati.
INSERT INTO users (name, email) VALUES (?, ?);
-- Associa ("Ada", "ada@example.com") -> esegui
-- Associa ("Boris", "boris@example.com") -> esegui
-- Associa ("Cyrus", NULL) -> esegui
SELECT id, name, email FROM users ORDER BY id;
La maggior parte dei driver lo racchiude in un executemany (Python) o in un ciclo di .run() (Node). In ogni caso risparmi il costo del parsing, che è piccolo per singola istruzione ma diventa reale quando inserisci migliaia di righe.
Non mescolare gli stili nella stessa istruzione
Tecnicamente SQLite consente placeholder posizionali e con nome nella stessa istruzione. Resisti alla tentazione.
-- Consentito ma pericoloso:
INSERT INTO users (name, email) VALUES (?, :email);
Chi legge deve seguire mentalmente due API di binding contemporaneamente, e la maggior parte dei driver non supporta bene la forma mista. Scegli uno stile per istruzione: ? per uno o due valori, :name per tutto il resto.
Un errore comune: il binding non è formattazione di stringhe
Il senso del binding è che i valori non passano dal parsing SQL. Confronta queste due righe di Python:
# Sbagliato: formattazione di stringhe:
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# Giusto: binding dei parametri:
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
La prima riga costruisce l'SQL per concatenazione. Se name è "'; DROP TABLE users; --", il database interpreta ed esegue tranquillamente l'istruzione iniettata. La seconda riga invia l'SQL e il valore attraverso canali diversi: il valore viene associato come stringa, punto, qualunque carattere contenga. Ecco perché ogni guida ti dice di usare il binding: non è una questione di stile, ma di cosa vede il parser.
Approfondiremo il lato injection nella prossima pagina.
Un altro errore: non puoi associare identificatori
I placeholder funzionano per i valori: stringhe, numeri, blob, NULL. Non funzionano per nomi di tabelle, nomi di colonne o parole chiave SQL:
-- Questo NON fa quello che vuoi:
SELECT * FROM ? WHERE id = ?;
-- Il primo ? viene associato come stringa letterale, non come nome di tabella.
Se ti serve davvero un nome di tabella o di colonna dinamico (raro nel codice applicativo), validalo con una lista di valori ammessi e concatenalo tu stesso nell'SQL, mai direttamente dall'input dell'utente. Per tutto il resto, usa il binding.
Un esempio completo
Mettiamo insieme i pezzi: una piccola tabella users scritta e letta interamente tramite binding:
Nel codice reale sia gli INSERT sia la SELECT userebbero i placeholder. La CLI semplicemente non ha un'app da cui fare il binding, quindi i letterali prendono il posto di ciò che il binding produrrebbe.
Prossimo passo: prevenire la SQL injection
Il binding dei parametri è il meccanismo. Perché blocca la SQL injection, e i pochi casi in cui il binding da solo non basta, è l'argomento della prossima pagina.
Domande frequenti
Cos'è il binding dei parametri in SQLite?
Il binding dei parametri è il modo in cui fornisci i valori a un prepared statement separatamente dal testo SQL. Scrivi un placeholder come ? o :name nell'SQL, poi passi il valore reale tramite l'API di bind del driver. SQLite tratta i valori associati solo come dati: non vengono mai interpretati come SQL.
Che differenza c'è tra ? e :name in SQLite?
? è un placeholder posizionale: i valori vengono associati nell'ordine in cui compaiono. :name (e @name, $name) sono placeholder con nome: associ il valore in base al nome invece che alla posizione. I parametri con nome sono più facili da leggere e da riordinare quando hai più di due o tre valori.
Come associo un valore NULL in SQLite?
Passa il valore null/None/nil del tuo linguaggio attraverso l'API di binding: i driver lo traducono automaticamente in NULL SQL. Non scrivere mai la stringa 'NULL' e non inserire mai la parola NULL nel testo SQL. Il senso del binding è proprio tenere i valori lontani dal parser SQL.
Posso mescolare parametri posizionali e con nome in un'istruzione?
SQLite lo consente, ma non farlo. Un'istruzione con placeholder sia ? sia :name è difficile da leggere e facile da associare in modo sbagliato. Scegli uno stile per istruzione, di solito i parametri con nome quando hai più di due o tre valori.