← Lập trình Python nâng cao

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":

input = "An"
Nối str (SAI) ok lần này
SELECT * FROM users WHERE name = 'An'
Tham số hoá (ĐÚNG) ✓ an toàn với mọi input
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.

★ Quy tắc vàng: ĐỪNG nối str input vào SQL. LUÔN tham số hoá. Đây là một trong các lỗ hổng phổ biến nhất (OWASP A03: Injection).
  • 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

Bạn đã lưu dữ liệu trong tệp và DB (kể cả hiệu năng). Mảnh "thế giới thật" cuối: nói chuyện qua mạng - bài kế tiếp Internet & HTTP (gọi & dựng API).

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.

ORM (vd SQLAlchemy) ánh xạ HÀNG ↔ OBJECT Python, nên bạn làm việc với object thay vì viết SQL tay. Lợi: gọn, an toàn kiểu, đỡ lặp. Đánh đổi: thêm một lớp trừu tượng, đôi khi SQL sinh ra chưa tối ưu. Học SQL thô trước (hiểu bản chất + đọc được EXPLAIN), rồi dùng ORM cho dự án lớn.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Đặc điểm nào đúng về SQLite?

  1. 1

    Bảng + Row factory

    Tạo bảng tasks; đặt row_factory = sqlite3.Row; thêm vài task bằng executemany; in theo tên cột.

    Hoàn thành khi: Dùng executemany cho chèn hàng loạt; đọc row["tieu_de"] (không phải row[1]).

  2. 2

    Tham số hoá

    Dùng widget Bước 3: thử input " ' OR '1'='1 ". Vì sao bản nối str trả MỌI user còn bản tham số hoá thì không?

    Hoàn thành khi: Nối str cho 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. 3

    Transaction

    Viết chuyển tiền trong with con: (trừ A, cộng B). Cố tình raise mộ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. 4

    Index & EXPLAIN

    Tạo bảng, chèn nhiều hàng; chạy EXPLAIN QUERY PLAN cho 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 khi CREATE INDEX: "SEARCH ... USING INDEX". Bạn giải thích khác biệt.

  5. 5

    SQL hay ORM?

    Bằng lời: script nhỏ vài bảng nên sqlite3 thô hay ORM? Web app lớn nhiều thực thể thì sao?

    Hoàn thành khi: Script nhỏ → sqlite3 thô đủ & rõ; app lớn nhiều quan hệ → ORM gọn & dễ bảo trì.