Menu

Wiązanie parametrów w SQLite: ?, :name i bezpieczne wartości

Jak działa wiązanie parametrów w SQLite: symbole zastępcze według pozycji, parametry nazwane i zasady bezpiecznego przekazywania wartości z aplikacji.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Wiązanie wprowadza wartości do prepared statement

Prepared statement to SQL z dziurami. Wiązanie (binding) to wypełnianie tych dziur wartościami: bezpiecznie, jedna po drugiej, przez API sterownika, a nie przez sklejanie ciągów znaków.

Schemat zawsze wygląda tak samo: piszesz SQL z symbolami zastępczymi, a wartości przekazujesz osobno.

W CLI nie da się tak naprawdę pokazać wiązania (powłoka nie ma podpiętego kodu aplikacji), ale powyższy SQL to dokładnie to, co wysyła twoja aplikacja. Znaki ? to symbole zastępcze. Twój sterownik (sqlite3 w Pythonie, better-sqlite3 w Node, rusqlite w Rust) wypełnia je osobnym wywołaniem bind.

Model myślowy: SQL to przepis, a powiązane wartości to składniki. Nigdy się nie mieszają.

Symbole zastępcze pozycyjne: ?

Najprostszy symbol zastępczy to ?. Każdy odpowiada kolejnej wiązanej wartości, po kolei.

INSERT INTO users (name, email) VALUES (?, ?);

W Pythonie wygląda to tak:

cursor.execute(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    ("Rosa", "rosa@example.com"),
)

Pierwszy ? dostaje "Rosa", drugi dostaje "rosa@example.com". Przekaż za mało lub za dużo wartości, a sterownik zgłosi błąd, zanim instrukcja się wykona.

Możesz też jawnie je numerować: ?1, ?2, ?3. To przydatne, gdy ta sama wartość występuje więcej niż raz:

SELECT ?1 AS greeting, ?1 AS still_the_same;

?1 używa ponownie pierwszej powiązanej wartości. Bez numerowania trzeba by wiązać tę samą wartość dwa razy.

Symbole zastępcze nazwane: :name

Gdy instrukcja ma więcej niż dwie lub trzy dziury, wiązanie pozycyjne zamienia się w zgadywankę. Parametry nazwane to naprawiają:

INSERT INTO users (name, email)
VALUES (:name, :email);

W Pythonie:

cursor.execute(
    "INSERT INTO users (name, email) VALUES (:name, :email)",
    {"name": "Boris", "email": "boris@example.com"},
)

Kolejność kluczy w słowniku nie ma znaczenia, liczą się tylko nazwy. SQLite akceptuje też prefiksy @name i $name jako alternatywy i wszystkie działają tak samo. :name jest zdecydowanie najpopularniejszy.

Parametry nazwane opłacają się w chwili, gdy masz UPDATE z pięcioma kolumnami albo zapytanie, które używa tej samej wartości w WHERE i RETURNING.

Wiązanie NULL

Właściwy sposób na wstawienie NULL to przekazanie wartości null swojego języka przez API wiązania. Sterownik zajmie się tłumaczeniem:

INSERT INTO users (name, email) VALUES (?, ?);
-- Bind: ("Cyrus", None)   w Pythonie
-- Bind: ["Cyrus", null]   w Node

SELECT id, name, email FROM users;

None, null, nil, jakkolwiek nazywa to twój język: sterownik zamienia to na prawdziwe SQL-owe NULL. Nie wiąż ciągu "NULL", bo zapiszesz czteroznakowy tekst "NULL". I nie wstawiaj słowa NULL do tekstu SQL, bo to całkowicie niweczy wiązanie.

Ta sama zasada dotyczy liczb, blobów i dat: przekazuj natywną wartość i pozwól sterownikowi ją powiązać.

Ponowne użycie instrukcji z innymi wartościami

Wiązanie w naturalny sposób łączy się z prepared statements. Przygotowujesz raz, wiążesz i wykonujesz wiele razy. Parser wykonuje swoją pracę jeden raz, a baza używa skompilowanego planu dla każdego zestawu powiązanych wartości.

INSERT INTO users (name, email) VALUES (?, ?);
-- Powiąż ("Ada",   "ada@example.com")    -> wykonaj
-- Powiąż ("Boris", "boris@example.com")  -> wykonaj
-- Powiąż ("Cyrus", NULL)                 -> wykonaj

SELECT id, name, email FROM users ORDER BY id;

Większość sterowników opakowuje to w executemany (Python) albo pętlę .run() (Node). Tak czy inaczej oszczędzasz koszt parsowania: mały dla jednej instrukcji, ale odczuwalny, gdy wstawiasz tysiące wierszy.

Nie mieszaj stylów w jednej instrukcji

Technicznie SQLite pozwala na symbole pozycyjne i nazwane w tej samej instrukcji. Oprzyj się pokusie.

-- Dozwolone, ale to proszenie się o kłopoty:
INSERT INTO users (name, email) VALUES (?, :email);

Czytelnik musi w głowie śledzić dwa API wiązania naraz, a większość sterowników nie obsługuje dobrze formy mieszanej. Wybierz jeden styl na instrukcję: ? dla jednej lub dwóch wartości, :name dla całej reszty.

Typowa pułapka: wiązanie to nie formatowanie ciągów

Cały sens wiązania polega na tym, że wartości nie przechodzą przez parsowanie SQL. Porównaj te dwie linie w Pythonie:

# Źle (formatowanie ciągów):
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

# Dobrze (wiązanie parametrów):
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))

Pierwsza linia buduje SQL przez konkatenację. Jeśli name to "'; DROP TABLE users; --", baza chętnie sparsuje i wykona wstrzykniętą instrukcję. Druga linia wysyła SQL i wartość różnymi kanałami: wartość jest wiązana jako ciąg znaków i koniec, niezależnie od tego, jakie znaki zawiera. Dlatego każdy poradnik każe wiązać: nie chodzi o styl, tylko o to, co widzi parser.

Stroną związaną z wstrzykiwaniem zajmiemy się na następnej stronie.

Kolejna pułapka: nie da się wiązać identyfikatorów

Symbole zastępcze działają dla wartości: ciągów, liczb, blobów, NULL. Nie działają dla nazw tabel, nazw kolumn ani słów kluczowych SQL:

-- To NIE robi tego, czego chcesz:
SELECT * FROM ? WHERE id = ?;
-- Pierwszy ? wiąże się jako literał tekstowy, a nie nazwa tabeli.

Jeśli naprawdę potrzebujesz dynamicznej nazwy tabeli lub kolumny (rzadkość w kodzie aplikacji), sprawdź ją względem listy dozwolonych wartości i sam wklej ją do SQL, nigdy prosto z danych od użytkownika. We wszystkich innych przypadkach wiąż.

Przykład w praktyce

Składając wszystko razem: mała tabela users zapisywana i odczytywana wyłącznie przez wiązanie:

W prawdziwym kodzie zarówno INSERT, jak i SELECT używałyby symboli zastępczych. CLI po prostu nie ma aplikacji, z której można wiązać, więc literały zastępują to, co dałoby wiązanie.

Dalej: ochrona przed SQL injection

Wiązanie parametrów to mechanizm. Dlaczego zatrzymuje SQL injection i w jakich nielicznych miejscach samo wiązanie nie wystarcza, o tym jest następna strona.

Najczęściej zadawane pytania

Czym jest wiązanie parametrów w SQLite?

Wiązanie parametrów to sposób przekazywania wartości do prepared statement oddzielnie od tekstu SQL. W SQL wpisujesz symbol zastępczy, na przykład ? lub :name, a właściwą wartość przekazujesz przez API wiązania sterownika. SQLite traktuje powiązane wartości wyłącznie jako dane i nigdy nie parsuje ich jako SQL.

Czym różni się ? od :name w SQLite?

? to symbol zastępczy pozycyjny: wartości są wiązane w kolejności wystąpienia. :name (oraz @name, $name) to symbole nazwane: wiążesz po nazwie, a nie po pozycji. Parametry nazwane łatwiej czytać i przestawiać, gdy masz więcej niż dwie lub trzy wartości.

Jak powiązać wartość NULL w SQLite?

Przekaż przez API wiązania wartość null/None/nil swojego języka, a sterownik automatycznie zamieni ją na SQL-owe NULL. Nigdy nie wpisuj ciągu 'NULL' i nigdy nie wstawiaj słowa NULL do tekstu SQL. Cały sens wiązania polega na tym, żeby wartości nie trafiały do parsera SQL.

Czy mogę mieszać parametry pozycyjne i nazwane w jednej instrukcji?

SQLite na to pozwala, ale nie rób tego. Instrukcję z symbolami ? i :name naraz trudno czytać i łatwo źle powiązać. Wybierz jeden styl na instrukcję, zwykle parametry nazwane, gdy masz więcej niż dwie lub trzy wartości.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ