Menu

Binding dei parametri in SQLite: ?, :name e valori sicuri

Come funziona il binding dei parametri in SQLite: placeholder posizionali, parametri con nome e le regole per passare valori in sicurezza dalla tua applicazione.

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

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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA