Bài 10 · Nâng cao · 26 phút· Cập nhật 11/06/2026
Cơ sở dữ liệu
Biên soạn bởi Nguyễn Anh Tuấn
Cơ sở dữ liệu với Python: sqlite3 & DB-API, transaction, truy vấn tham số hoá chống SQL injection, index - và nhập môn ORM.
Tệp hợp dữ liệu nhỏ. Khi cần truy vấn linh hoạt, nhiều ghi đồng thời, và toàn vẹn, hãy dùng CSDL. SQLite rất hợp với người mới: một tệp, không server, sqlite3 có sẵn.
- ▸DB cho: truy vấn (lọc/sắp/nối), nhiều ghi đồng thời, ràng buộc & transaction.
- ▸SQLite = CSDL một-tệp, không server; sqlite3 có sẵn - khỏi cài gì.
- ▸Quy mô lớn/nhiều máy mới chuyển sang Postgres/MySQL (cùng kiểu DB-API).
db_basic.py
import sqlite3
con = sqlite3.connect("app.db")
con.row_factory = sqlite3.Row # truy cap theo TEN COT
con.execute("""CREATE TABLE IF NOT EXISTS tasks(
id INTEGER PRIMARY KEY, tieu_de TEXT, xong INTEGER)""")
con.executemany( # chen NHIEU hang mot lan
"INSERT INTO tasks(tieu_de, xong) VALUES (?, ?)",
[("Hoc SQL", 0), ("Viet test", 0), ("Doc sach", 1)],
)
con.commit()
for row in con.execute("SELECT * FROM tasks WHERE xong = 0"):
print(row["id"], row["tieu_de"]) # theo ten cot, khong phai row[1]
con.close() Kết quả khi chạy
1 Hoc SQL 2 Viet test
- ▸connect mở/tạo file DB; execute chạy SQL; commit để LƯU (quên commit = mất).
- ▸row_factory = sqlite3.Row → đọc theo tên cột (row["tieu_de"]) - rõ & bền khi đổi cột.
- ▸executemany(sql, list) chèn hàng loạt gọn & nhanh hơn lặp execute.
KHÔNG bao giờ nối str input vào SQL. Dùng dấu ? để truyền input như dữ liệu. Thử nhập một giá trị tấn công:
Chọn một giá trị người dùng nhập vào ô "name":
SELECT * FROM users WHERE name = 'An'
cur.execute(SELECT * FROM users WHERE name = ?, (name,))
input được TRUYỀN RIÊNG như dữ liệu (qua dấu ?) → DB không bao giờ thực thi nó như SQL, dù chứa nháy đơn hay lệnh.
- ▸SAI: f"... WHERE name = '{input}'" - input có thể "thoát" và chèn SQL.
- ▸ĐÚNG: execute("... WHERE name = ?", (input,)) - DB coi input là dữ liệu thuần.
- ▸Đây là OWASP A03 (Injection) - luôn tham số hoá, kể cả với dữ liệu "có vẻ an toàn".
transaction.py - chuyển tiền nguyên tử
def chuyen_tien(con, tu, den, so):
with con: # transaction: commit neu xuoi, rollback neu loi
con.execute("UPDATE tk SET sodu = sodu - ? WHERE id = ?", (so, tu))
con.execute("UPDATE tk SET sodu = sodu + ? WHERE id = ?", (so, den))
# mot lenh loi -> rollback CA HAI -> khong mat tien lung lo - ▸with con: tự commit khi thoát êm, rollback khi có ngoại lệ - gọn & an toàn.
- ▸Transaction = "toàn bộ hoặc không gì": commit lưu hết, rollback huỷ hết.
- ▸Đặt RÀNG BUỘC ở DB (NOT NULL, UNIQUE, CHECK, FOREIGN KEY) làm "lưới cuối".
Bảng lớn dần thì truy vấn lọc/nối có thể chậm vì DB phải quét cả bảng. Index (như mục lục sách) giúp tìm nhanh; EXPLAIN QUERY PLAN cho biết DB sẽ làm gì:
index.py
# Chua co index tren cot 'ten':
con.execute("EXPLAIN QUERY PLAN SELECT * FROM t WHERE ten = ?", ("An",)).fetchone()
# -> (..., 'SCAN t') # quet CA bang (cham khi bang lon)
con.execute("CREATE INDEX idx_ten ON t(ten)")
# Sau khi co index:
con.execute("EXPLAIN QUERY PLAN SELECT * FROM t WHERE ten = ?", ("An",)).fetchone()
# -> (..., 'SEARCH t USING INDEX idx_ten (ten=?)') # dung index (nhanh) Kết quả khi chạy
SCAN t SEARCH t USING INDEX idx_ten (ten=?)
- ▸Không index → "SCAN" (quét cả bảng); có index trên cột hay lọc/nối → "SEARCH ... USING INDEX".
- ▸Tạo index cho cột thường xuất hiện ở WHERE/JOIN/ORDER BY.
- ▸Đừng index bừa: mỗi index làm INSERT/UPDATE chậm hơn & tốn chỗ - index theo nhu cầu truy vấn.
Viết SQL tay rõ ràng nhưng lặp. ORM (vd SQLAlchemy) ánh xạ hàng ↔ object, để bạn làm việc với object Python:
orm_y_tuong.py (ý tưởng, SQLAlchemy)
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
tieu_de: Mapped[str]
xong: Mapped[bool] = mapped_column(default=False)
session.add(Task(tieu_de="Hoc ORM"))
session.commit()
ds = session.query(Task).filter_by(xong=False).all() - ▸ORM ánh xạ hàng-bảng ↔ object → ít SQL tay, gọn, an toàn kiểu.
- ▸Đánh đổi: thêm lớp trừu tượng; đôi khi SQL sinh ra chưa tối ưu (đọc EXPLAIN để kiểm).
- ▸Học SQL thô trước, dùng ORM cho dự án lớn nhiều thực thể.
Tiếp theo
Câu hỏi thường gặp
Khi cần TRUY VẤN linh hoạt (lọc/sắp/nối nhiều bảng), nhiều người/luồng cùng ghi, hoặc cần TOÀN VẸN (transaction, ràng buộc). Tệp JSON/CSV hợp dữ liệu nhỏ, đọc cả khối. DB hợp dữ liệu lớn dần, truy vấn thường xuyên.
SQLite là CSDL gọn nằm trong MỘT TỆP, KHÔNG cần server riêng - và module sqlite3 có SẴN trong Python. Rất hợp app desktop/CLI, prototype, test. Cần nhiều kết nối ghi cao hoặc nhiều máy mới chuyển sang Postgres/MySQL.
Mặc định mỗi hàng trả về là một TUPLE (truy cập theo chỉ số: row[0]). Đặt con.row_factory = sqlite3.Row cho phép truy cập theo TÊN CỘT (row["ten"]) - rõ ràng & ít lỗi khi đổi thứ tự cột. Vẫn truy cập theo chỉ số được.
Vì nó chống SQL INJECTION - lỗ hổng nguy hiểm hàng đầu. Nối str input vào SQL cho phép kẻ xấu "thoát" khỏi str và chèn lệnh (lộ dữ liệu, xoá bảng). Tham số hoá (dấu ?) truyền input như DỮ LIỆU, DB không bao giờ thực thi nó như SQL. Luôn tham số hoá - không ngoại lệ.
Không có index, DB phải QUÉT TOÀN BẢNG (full scan) để tìm - chậm khi bảng lớn. Index trên cột hay lọc/nối giúp tìm nhanh (như mục lục sách). EXPLAIN QUERY PLAN cho biết DB sẽ chạy truy vấn ra sao: "SCAN" = quét cả bảng; "SEARCH ... USING INDEX" = dùng index (nhanh). Dùng nó để kiểm truy vấn có dùng index không.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Đặc điểm nào đúng về SQLite?
- 1
Bảng + Row factory
Tạo bảng
tasks; đặtrow_factory = sqlite3.Row; thêm vài task bằngexecutemany; in theo tên cột.Hoàn thành khi: Dùng
executemanycho chèn hàng loạt; đọcrow["tieu_de"](không phảirow[1]). - 2
Tham số hoá
Dùng widget Bước 3: thử input
" ' OR '1'='1 ". Vì sao bản nốistrtrả MỌI user còn bản tham số hoá thì không?Hoàn thành khi: Nối
strcho input "thoát" thành SQL; tham số hoá giữ input là dữ liệu → tìm đúng tên đó (không có) → 0 kết quả. - 3
Transaction
Viết chuyển tiền trong
with con:(trừ A, cộng B). Cố tìnhraisemột ngoại lệ giữa chừng; kiểm dữ liệu KHÔNG đổi.Hoàn thành khi: Có lỗi → rollback tự động → số dư cả hai như cũ.
- 4
Index & EXPLAIN
Tạo bảng, chèn nhiều hàng; chạy
EXPLAIN QUERY PLANcho một truy vấn lọc theo cột chưa có index; tạo index rồi chạy lại.Hoàn thành khi: Trước:
"SCAN"; sau khiCREATE INDEX:"SEARCH ... USING INDEX". Bạn giải thích khác biệt. - 5
SQL hay ORM?
Bằng lời: script nhỏ vài bảng nên
sqlite3thô hay ORM? Web app lớn nhiều thực thể thì sao?Hoàn thành khi: Script nhỏ →
sqlite3thô đủ & rõ; app lớn nhiều quan hệ → ORM gọn & dễ bảo trì.