Menu

Gắn tham số trong SQLite: ?, :name và truyền giá trị an toàn

Cách gắn tham số (parameter binding) hoạt động trong SQLite: placeholder theo vị trí, tham số có tên và các quy tắc để truyền giá trị an toàn từ ứng dụng.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Gắn tham số là cách đưa giá trị vào prepared statement

Một prepared statement là câu SQL có những chỗ trống. Gắn tham số (binding) là việc lấp các chỗ trống đó bằng giá trị: an toàn, từng giá trị một, thông qua API của driver thay vì ghép chuỗi lại với nhau.

Hình dạng lúc nào cũng giống nhau: viết câu SQL với các placeholder, rồi đưa giá trị vào riêng.

Trong CLI bạn không thể thực sự minh họa việc gắn tham số (shell không có code ứng dụng đi kèm), nhưng câu SQL ở trên chính xác là thứ ứng dụng của bạn gửi đi. Các dấu ? là placeholder. Driver của bạn, sqlite3 trong Python, better-sqlite3 trong Node, rusqlite trong Rust, sẽ lấp chúng bằng một lời gọi bind riêng.

Mô hình cần nhớ: câu SQL là công thức nấu ăn, các giá trị được gắn là nguyên liệu. Chúng không bao giờ trộn lẫn vào nhau.

Placeholder theo vị trí: ?

Placeholder đơn giản nhất là ?. Mỗi dấu khớp với giá trị tiếp theo bạn gắn, theo thứ tự.

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

Trong Python sẽ là:

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

Dấu ? đầu tiên nhận "Rosa", dấu thứ hai nhận "rosa@example.com". Truyền thiếu hoặc thừa giá trị, driver sẽ báo lỗi trước khi câu lệnh chạy.

Bạn cũng có thể đánh số rõ ràng bằng ?1, ?2, ?3, rất hữu ích khi cùng một giá trị xuất hiện nhiều lần:

SELECT ?1 AS greeting, ?1 AS still_the_same;

?1 dùng lại giá trị được gắn đầu tiên. Không đánh số, bạn sẽ phải gắn cùng một giá trị hai lần.

Placeholder có tên: :name

Khi một câu lệnh có nhiều hơn hai hoặc ba chỗ trống, gắn theo vị trí biến thành trò đoán mò. Tham số có tên giải quyết chuyện đó:

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

Trong Python:

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

Thứ tự các khóa trong từ điển không quan trọng, chỉ tên mới quan trọng. SQLite cũng chấp nhận @name và $name làm tiền tố thay thế; tất cả đều hoạt động như nhau. :name là kiểu phổ biến nhất.

Tham số có tên phát huy tác dụng ngay khi bạn có một lệnh UPDATE với năm cột, hoặc một truy vấn dùng cùng một giá trị trong WHERE và RETURNING.

Gắn NULL

Cách đúng để chèn NULL là truyền giá trị null của ngôn ngữ bạn dùng qua API gắn tham số. Driver sẽ lo phần chuyển đổi:

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, ngôn ngữ của bạn gọi nó là gì cũng được: driver biến nó thành một NULL thật của SQL. Đừng gắn chuỗi "NULL"; làm vậy sẽ lưu văn bản bốn ký tự "NULL". Và đừng chèn chữ NULL vào văn bản SQL: làm vậy là phá bỏ hoàn toàn việc gắn tham số.

Quy tắc tương tự áp dụng cho số, blob, ngày tháng: truyền giá trị gốc, để driver gắn nó.

Dùng lại một câu lệnh với các giá trị khác nhau

Gắn tham số đi đôi tự nhiên với prepared statement. Chuẩn bị một lần, gắn và thực thi nhiều lần. Bộ phân tích chỉ làm việc một lần duy nhất và cơ sở dữ liệu dùng lại kế hoạch đã biên dịch cho mọi bộ giá trị được gắn.

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

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

Phần lớn driver bọc việc này trong executemany (Python) hoặc một vòng lặp .run() (Node). Dù bằng cách nào, thứ bạn tiết kiệm được là chi phí phân tích: nhỏ với mỗi câu lệnh nhưng đáng kể khi bạn chèn hàng nghìn hàng.

Đừng trộn hai kiểu trong một câu lệnh

Về mặt kỹ thuật, SQLite cho phép placeholder theo vị trí và có tên trong cùng một câu lệnh. Đừng làm vậy.

-- Legal but a footgun:
INSERT INTO users (name, email) VALUES (?, :email);

Người đọc sẽ phải theo dõi cùng lúc hai cách gắn tham số, và phần lớn driver không hỗ trợ tốt dạng trộn này. Hãy chọn một kiểu cho mỗi câu lệnh: ? cho một hoặc hai giá trị, :name cho mọi trường hợp còn lại.

Một lỗi thường gặp: gắn tham số không phải định dạng chuỗi

Mục đích của việc gắn tham số là các giá trị không đi qua bộ phân tích SQL. Hãy so sánh hai dòng Python này:

# Wrong - string formatting:
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

# Right - parameter binding:
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))

Dòng đầu tiên tạo câu SQL bằng cách nối chuỗi. Nếu name là "'; DROP TABLE users; --", cơ sở dữ liệu sẽ vui vẻ phân tích và chạy câu lệnh bị chèn vào. Dòng thứ hai gửi câu SQL và giá trị qua hai kênh khác nhau: giá trị được gắn như một chuỗi, chấm hết, bất kể nó chứa ký tự gì. Đó là lý do mọi hướng dẫn đều bảo bạn gắn tham số: chuyện này không phải về phong cách, mà là về thứ bộ phân tích nhìn thấy.

Chúng ta sẽ đi sâu vào khía cạnh injection của vấn đề này ở trang tiếp theo.

Một lỗi khác: không thể gắn định danh

Placeholder dùng cho giá trị: chuỗi, số, blob, NULL. Chúng không dùng được cho tên bảng, tên cột hay từ khóa SQL:

-- This does NOT do what you want:
SELECT * FROM ? WHERE id = ?;
-- The first ? binds as a string literal, not a table name.

Nếu bạn thật sự cần tên bảng hoặc tên cột động (hiếm gặp trong code ứng dụng), hãy kiểm tra nó với một danh sách cho phép (allow-list) rồi tự nối vào câu SQL, tuyệt đối không lấy trực tiếp từ dữ liệu người dùng nhập. Với mọi thứ khác, hãy gắn tham số.

Một ví dụ hoàn chỉnh

Ghép các phần lại với nhau: một bảng users nhỏ được ghi và đọc hoàn toàn thông qua gắn tham số:

Trong code thật, các lệnh INSERT và SELECT đều sẽ dùng placeholder. Chỉ là CLI không có ứng dụng nào để gắn tham số, nên các giá trị viết trực tiếp đứng thay cho kết quả mà việc gắn tham số tạo ra.

Tiếp theo: ngăn chặn SQL injection

Gắn tham số là cơ chế. Vì sao nó ngăn được SQL injection, và một vài chỗ mà chỉ gắn tham số thôi là chưa đủ, là nội dung của trang tiếp theo.

Câu hỏi thường gặp

Gắn tham số trong SQLite là gì?

Gắn tham số là cách bạn cung cấp giá trị cho một prepared statement tách biệt khỏi văn bản SQL. Bạn viết một placeholder như ? hoặc :name trong câu SQL, rồi truyền giá trị thật qua API bind của driver. SQLite coi các giá trị được gắn thuần túy là dữ liệu: chúng không bao giờ bị phân tích như SQL.

? và :name trong SQLite khác nhau thế nào?

? là placeholder theo vị trí: các giá trị được gắn theo thứ tự xuất hiện. :name (cùng với @name, $name) là placeholder có tên: bạn gắn theo tên thay vì theo vị trí. Tham số có tên dễ đọc và dễ đổi thứ tự hơn khi bạn có nhiều hơn hai hoặc ba giá trị.

Làm sao gắn giá trị NULL trong SQLite?

Truyền giá trị null/None/nil của ngôn ngữ bạn dùng qua API gắn tham số: driver tự động chuyển nó thành NULL của SQL. Đừng bao giờ viết chuỗi 'NULL', và đừng bao giờ chèn chữ NULL vào văn bản SQL. Mục đích của việc gắn tham số chính là giữ các giá trị nằm ngoài bộ phân tích SQL.

Có thể trộn tham số theo vị trí và tham số có tên trong một câu lệnh không?

SQLite cho phép, nhưng đừng làm vậy. Một câu lệnh có cả placeholder ? và :name khó đọc và dễ gắn sai. Hãy chọn một kiểu cho mỗi câu lệnh: thường là tham số có tên khi bạn có nhiều hơn hai hoặc ba giá trị.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu