Hiểu chuẩn DB-API 2.0 — giao diện chung của mọi thư viện database Python
Thao tác với SQLite (có sẵn, không cần cài)
Biết cách kết nối SQL Server và PostgreSQL
Hiểu và tránh SQL Injection
Thời lượng: ~5 giờ | Tuần: 25
Module này tận dụng trực tiếp kiến thức T-SQL/PostgreSQL sẵn có của bạn.
1. DB-API 2.0 — Chuẩn chung
Python có một chuẩn thống nhất cho mọi thư viện database, gọi là DB-API 2.0 (đặc tả tại PEP 249).
Nhờ chuẩn này, code làm việc với SQLite, SQL Server, PostgreSQL, MySQL gần như giống hệt nhau — chỉ khác câu lệnh kết nối.
Luồng làm việc chuẩn
graph LR
A[connect<br/>Kết nối] --> B[cursor<br/>Tạo con trỏ]
B --> C[execute<br/>Chạy SQL]
C --> D{Loại lệnh?}
D -->|SELECT| E[fetchall / fetchone<br/>Lấy dữ liệu]
D -->|INSERT/UPDATE/DELETE| F[commit<br/>Lưu thay đổi]
E --> G[close<br/>Đóng]
F --> G
Bước
Đối tượng
Vai trò
1
Connection
Kết nối tới database
2
Cursor
Con trỏ — thực thi lệnh và giữ kết quả
3
execute()
Chạy câu SQL
4
fetchone() / fetchall()
Lấy dữ liệu về
5
commit() / rollback()
Xác nhận / hủy giao dịch
6
close()
Đóng kết nối
Cursor trong Python vs T-SQL
Từ “cursor” ở đây khác với DECLARE CURSOR trong T-SQL (thứ mà DBA thường khuyên tránh vì chậm). Cursor trong DB-API chỉ là đối tượng để gửi lệnh và nhận kết quả — nó không gây vấn đề hiệu năng như cursor T-SQL.
2. SQLite — Bắt đầu ở đây
SQLite là database nhúng, toàn bộ nằm trong một file duy nhất, và Python đã cài sẵn module sqlite3 — không cần cài gì thêm.
Đây là môi trường lý tưởng để học, trước khi đụng tới SQL Server thật.
2.1. Tạo database và bảng
import sqlite3# Kết nối — file tự tạo nếu chưa cóconn = sqlite3.connect("ban_hang.db")cur = conn.cursor()cur.execute(""" CREATE TABLE IF NOT EXISTS KhachHang ( MaKH TEXT PRIMARY KEY, TenKH TEXT NOT NULL, Tinh TEXT, CongNo REAL DEFAULT 0, NgayTao TEXT DEFAULT (date('now')), HoatDong INTEGER DEFAULT 1 )""")conn.commit()conn.close()print("Da tao database va bang")
2.2. Dùng with để tự đóng kết nối
import sqlite3with sqlite3.connect("ban_hang.db") as conn: cur = conn.cursor() cur.execute("SELECT COUNT(*) FROM KhachHang") print(cur.fetchone()[0])
with của sqlite3 chỉ tự COMMIT, KHÔNG tự đóng kết nối
Đây là điểm khác biệt của sqlite3 so với open(). Khi ra khỏi khối with, nó tự commit() (hoặc rollback() nếu có lỗi) nhưng không gọi close().
Cách viết an toàn nhất:
conn = sqlite3.connect("ban_hang.db")try: with conn: # Tự commit/rollback cur = conn.cursor() cur.execute(...)finally: conn.close() # Tự đóng
2.3. Kiểu dữ liệu SQLite
SQLite chỉ có 5 kiểu lưu trữ. Bảng ánh xạ với Python:
SQLite
Python
Ghi chú
NULL
None
INTEGER
int
REAL
float
⚠️ Có sai số — không dùng cho tiền
TEXT
str
BLOB
bytes
Dữ liệu nhị phân
SQLite không có kiểu DECIMAL, DATE, BOOLEAN
Tiền tệ: lưu dạng TEXT rồi chuyển sang Decimal, hoặc lưu INTEGER theo đơn vị nhỏ nhất (đồng)
Ngày tháng: lưu dạng TEXT định dạng ISO "2026-08-08"
Boolean: lưu INTEGER 0/1
Đây là điểm SQLite thua xa SQL Server/PostgreSQL. Với dự án thật về kế toán, dùng PostgreSQL hoặc SQL Server.
3. ⚠️ Parameterized Query — Chống SQL Injection
Đây là phần quan trọng nhất của module này.
3.1. Cách SAI — Nối chuỗi
# ❌ TUYỆT ĐỐI KHÔNG LÀMma_kh = input("Nhap ma KH: ")cur.execute(f"SELECT * FROM KhachHang WHERE MaKH = '{ma_kh}'")
Nếu người dùng nhập:
' OR '1'='1
Câu SQL trở thành:
SELECT * FROM KhachHang WHERE MaKH = '' OR '1'='1'
→ Trả về toàn bộ khách hàng.
Tệ hơn, nếu nhập:
'; DROP TABLE KhachHang; --
→ Xóa sạch bảng.
Đây là SQL Injection — lỗ hổng bảo mật phổ biến và nguy hiểm nhất trong ứng dụng có database.
3.2. Cách ĐÚNG — Truyền tham số
# ✅ ĐÚNG — dùng placeholder ?ma_kh = input("Nhap ma KH: ")cur.execute("SELECT * FROM KhachHang WHERE MaKH = ?", (ma_kh,))
Thư viện database sẽ escape giá trị đúng cách. Dữ liệu người dùng nhập không bao giờ được coi là mã SQL.
Chú ý dấu phẩy trong (ma_kh,)
Tham số phải là tuple. Với một tham số duy nhất, bắt buộc có dấu phẩy:
cur.execute("... WHERE MaKH = ?", (ma_kh,)) # ✅ tuple 1 phần tửcur.execute("... WHERE MaKH = ?", (ma_kh)) # ❌ đây là chuỗi, không phải tuplecur.execute("... WHERE MaKH = ?", [ma_kh]) # ✅ list cũng được
Đọc tài liệu của thư viện để biết paramstyle của nó.
3.4. Tham số theo tên
cur.execute( "SELECT * FROM KhachHang WHERE Tinh = :tinh AND CongNo > :nguong", {"tinh": "Hue", "nguong": 1_000_000})
Dễ đọc hơn khi có nhiều tham số.
3.5. Điều KHÔNG truyền tham số được
Placeholder chỉ dùng cho giá trị, không dùng cho tên bảng, tên cột, từ khóa:
# ❌ Không hoạt độngcur.execute("SELECT * FROM ? WHERE ? = ?", (bang, cot, gia_tri))# ✅ Với tên bảng/cột động: dùng danh sách trắngCOT_HOP_LE = {"MaKH", "TenKH", "Tinh", "CongNo"}if cot not in COT_HOP_LE: raise ValueError(f"Cot khong hop le: {cot}")cur.execute(f"SELECT * FROM KhachHang ORDER BY {cot}") # An toàn vì đã kiểm tra
4. CRUD — Bốn thao tác cơ bản
4.1. INSERT
import sqlite3conn = sqlite3.connect("ban_hang.db")cur = conn.cursor()# Thêm 1 bản ghicur.execute( "INSERT INTO KhachHang (MaKH, TenKH, Tinh, CongNo) VALUES (?, ?, ?, ?)", ("KH001", "Nguyễn Văn A", "Huế", 5_000_000))# Thêm NHIỀU bản ghi — executemanydu_lieu = [ ("KH002", "Trần Thị B", "TP HCM", 12_000_000), ("KH003", "Lê Văn C", "Hà Nội", 0), ("KH004", "Phạm Thị D", "Huế", 8_500_000),]cur.executemany( "INSERT INTO KhachHang (MaKH, TenKH, Tinh, CongNo) VALUES (?, ?, ?, ?)", du_lieu)conn.commit() # ⚠️ BẮT BUỘC — không commit là mất dữ liệuprint(f"Da them {cur.rowcount} ban ghi")conn.close()
Quên commit() = mất dữ liệu
DB-API mặc định chạy trong transaction. Không gọi commit() thì mọi thay đổi bị hủy khi đóng kết nối. Đây là lỗi phổ biến nhất của người mới.
4.2. SELECT
conn = sqlite3.connect("ban_hang.db")cur = conn.cursor()# Lấy TẤT CẢcur.execute("SELECT MaKH, TenKH, CongNo FROM KhachHang ORDER BY CongNo DESC")tat_ca = cur.fetchall()print(tat_ca)# [('KH002', 'Trần Thị B', 12000000.0), ('KH004', ...), ...]# Lấy MỘT dòngcur.execute("SELECT * FROM KhachHang WHERE MaKH = ?", ("KH001",))mot_dong = cur.fetchone()print(mot_dong) # ('KH001', 'Nguyễn Văn A', 'Huế', 5000000.0, ...)# Trả về None nếu không tìm thấy# Lấy N dòngcur.execute("SELECT * FROM KhachHang")ba_dong = cur.fetchmany(3)# Duyệt trực tiếp — tiết kiệm bộ nhớ nhấtcur.execute("SELECT MaKH, TenKH, CongNo FROM KhachHang")for ma, ten, no in cur: print(f"{ma:<8}{ten:<22}{no:>15,.0f}")conn.close()
4.3. Truy cập theo tên cột — sqlite3.Row
Mặc định kết quả là tuple, phải nhớ thứ tự cột. Cách tốt hơn:
conn = sqlite3.connect("ban_hang.db")conn.row_factory = sqlite3.Row # ⭐ Bật chế độ truy cập theo têncur = conn.cursor()cur.execute("SELECT * FROM KhachHang WHERE Tinh = ?", ("Huế",))for dong in cur: print(dong["MaKH"], dong["TenKH"], dong["CongNo"]) # Theo TÊN cột# Chuyển sang list các dict — cấu trúc quen thuộc từ M09cur.execute("SELECT * FROM KhachHang")kq = [dict(d) for d in cur.fetchall()]print(kq[0])# {'MaKH': 'KH001', 'TenKH': 'Nguyễn Văn A', 'Tinh': 'Huế', 'CongNo': 5000000.0, ...}conn.close()
list các dict = result set. Sau khi lấy về, mọi kỹ thuật lọc, sắp xếp, group by bằng Python bạn đã học đều áp dụng được.
4.4. UPDATE và DELETE
conn = sqlite3.connect("ban_hang.db")cur = conn.cursor()# UPDATEcur.execute( "UPDATE KhachHang SET CongNo = CongNo - ? WHERE MaKH = ?", (2_000_000, "KH001"))print(f"Da cap nhat {cur.rowcount} dong")# DELETEcur.execute("DELETE FROM KhachHang WHERE CongNo = 0 AND HoatDong = 0")print(f"Da xoa {cur.rowcount} dong")conn.commit()conn.close()
cur.rowcount cho biết số dòng bị ảnh hưởng — tương đương @@ROWCOUNT trong T-SQL.
5. Transaction — Giao dịch
import sqlite3conn = sqlite3.connect("ban_hang.db")cur = conn.cursor()try: # Nghiệp vụ: chuyển công nợ từ KH này sang KH khác cur.execute("UPDATE KhachHang SET CongNo = CongNo - ? WHERE MaKH = ?", (1_000_000, "KH001")) cur.execute("UPDATE KhachHang SET CongNo = CongNo + ? WHERE MaKH = ?", (1_000_000, "KH002")) conn.commit() # Cả 2 lệnh cùng thành công print("Giao dich thanh cong")except sqlite3.Error as e: conn.rollback() # Có lỗi -> hủy TOÀN BỘ print(f"Giao dich that bai, da rollback: {e}")finally: conn.close()
Khái niệm transaction giống hệt BEGIN TRAN / COMMIT / ROLLBACK trong T-SQL — bạn đã quen.
6. Hàm tiện ích tự viết
"""Lớp bọc đơn giản cho thao tác database SQLite."""import sqlite3from contextlib import contextmanagerfrom pathlib import PathDUONG_DAN_DB = Path("ban_hang.db")@contextmanagerdef ket_noi(duong_dan=DUONG_DAN_DB): """ Context manager quản lý kết nối database. Tự động commit khi thành công, rollback khi lỗi, và luôn đóng kết nối. Sử dụng: with ket_noi() as conn: cur = conn.cursor() cur.execute(...) """ conn = sqlite3.connect(duong_dan) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") # Bật ràng buộc khóa ngoại try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close()def truy_van(sql, tham_so=()): """ Chạy câu SELECT, trả về list các dict. Tham số: sql (str): Câu SQL với placeholder ?. tham_so (tuple): Các giá trị thay cho placeholder. Trả về: list[dict]: Kết quả truy vấn. """ with ket_noi() as conn: cur = conn.cursor() cur.execute(sql, tham_so) return [dict(d) for d in cur.fetchall()]def truy_van_mot(sql, tham_so=()): """Chạy SELECT, trả về một dict hoặc None.""" with ket_noi() as conn: cur = conn.cursor() cur.execute(sql, tham_so) dong = cur.fetchone() return dict(dong) if dong else Nonedef thuc_thi(sql, tham_so=()): """ Chạy INSERT/UPDATE/DELETE. Trả về: int: Số dòng bị ảnh hưởng. """ with ket_noi() as conn: cur = conn.cursor() cur.execute(sql, tham_so) return cur.rowcountdef thuc_thi_nhieu(sql, danh_sach_tham_so): """Chạy một câu lệnh với nhiều bộ tham số.""" with ket_noi() as conn: cur = conn.cursor() cur.executemany(sql, danh_sach_tham_so) return cur.rowcount# ==================== SỬ DỤNG ====================if __name__ == "__main__": # Truy vấn kh_hue = truy_van( "SELECT MaKH, TenKH, CongNo FROM KhachHang WHERE Tinh = ? ORDER BY CongNo DESC", ("Huế",) ) for kh in kh_hue: print(f"{kh['MaKH']:<8}{kh['TenKH']:<22}{kh['CongNo']:>15,.0f}") # Một dòng kh = truy_van_mot("SELECT * FROM KhachHang WHERE MaKH = ?", ("KH001",)) if kh: print(f"\nTim thay: {kh['TenKH']}") else: print("\nKhong tim thay") # Cập nhật so_dong = thuc_thi( "UPDATE KhachHang SET CongNo = ? WHERE MaKH = ?", (0, "KH003") ) print(f"Da cap nhat {so_dong} dong")
7. Kết nối SQL Server
7.1. Cài đặt
pip install pyodbc
Cần cài thêm ODBC Driver for SQL Server trên máy (tải từ trang Microsoft).
7.2. Kết nối
import pyodbcchuoi_ket_noi = ( "DRIVER={ODBC Driver 18 for SQL Server};" "SERVER=localhost;" "DATABASE=QuanLyBanHang;" "UID=sa;" "PWD=mat_khau;" "TrustServerCertificate=yes;")conn = pyodbc.connect(chuoi_ket_noi)cur = conn.cursor()cur.execute("SELECT TOP 10 MaKH, TenKH, CongNo FROM KhachHang ORDER BY CongNo DESC")for dong in cur: print(dong.MaKH, dong.TenKH, dong.CongNo) # Truy cập theo tên cộtconn.close()
cur.execute("{CALL sp_BaoCaoCongNo (?, ?)}", ("2026-08-01", "2026-08-31"))for dong in cur.fetchall(): print(dong)
8. Kết nối PostgreSQL
8.1. Cài đặt
pip install "psycopg[binary]"
8.2. Kết nối
import psycopgfrom psycopg.rows import dict_rowwith psycopg.connect( host="localhost", port=5432, dbname="quanlybanhang", user="postgres", password="mat_khau", row_factory=dict_row, # Trả về dict thay vì tuple) as conn: with conn.cursor() as cur: cur.execute( "SELECT ma_kh, ten_kh, cong_no FROM khach_hang " "WHERE tinh = %s ORDER BY cong_no DESC LIMIT 10", ("Huế",) ) for dong in cur: print(dong["ma_kh"], dong["ten_kh"], dong["cong_no"])
Chú ý placeholder của psycopg là %s, không phải ?.
psycopg xử lý Decimal đúng
PostgreSQL có kiểu NUMERIC, và psycopg tự chuyển thành Decimal của Python — không mất chính xác. Đây là ưu điểm lớn so với SQLite khi làm phần mềm tài chính.
9. Bảo mật thông tin kết nối
KHÔNG viết mật khẩu trực tiếp trong code
# ❌ TUYỆT ĐỐI KHÔNGconn = psycopg.connect(password="MatKhau123!")
Code này khi đưa lên Git là mật khẩu lộ vĩnh viễn trong lịch sử commit.
"""Hệ thống quản lý khách hàng với SQLite.Tổng hợp toàn bộ kiến thức từ M02 đến M16."""import sqlite3from contextlib import contextmanagerfrom datetime import datefrom decimal import Decimalfrom pathlib import PathDB = Path("khach_hang.db")# ==================== TẦNG DATABASE ====================@contextmanagerdef ket_noi(): """Context manager quản lý kết nối.""" conn = sqlite3.connect(DB) conn.row_factory = sqlite3.Row try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close()def khoi_tao_db(): """Tạo bảng nếu chưa tồn tại.""" with ket_noi() as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS KhachHang ( MaKH TEXT PRIMARY KEY, TenKH TEXT NOT NULL, Tinh TEXT, CongNo TEXT DEFAULT '0', NgayTao TEXT NOT NULL, HoatDong INTEGER DEFAULT 1 ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_kh_tinh ON KhachHang(Tinh) """)# ==================== TẦNG NGHIỆP VỤ ====================def them_khach_hang(ma, ten, tinh, cong_no=Decimal("0")): """Thêm khách hàng mới. Ném ValueError nếu mã đã tồn tại.""" if not ma or not ma.strip(): raise ValueError("Ma khach hang khong duoc de trong") if not ten or not ten.strip(): raise ValueError("Ten khach hang khong duoc de trong") try: with ket_noi() as conn: conn.execute( "INSERT INTO KhachHang (MaKH, TenKH, Tinh, CongNo, NgayTao) " "VALUES (?, ?, ?, ?, ?)", (ma.strip().upper(), ten.strip(), tinh.strip(), str(cong_no), date.today().isoformat()) ) except sqlite3.IntegrityError: raise ValueError(f"Ma khach hang '{ma}' da ton tai")def lay_tat_ca(chi_hoat_dong=True): """Lấy danh sách khách hàng.""" sql = "SELECT * FROM KhachHang" tham_so = () if chi_hoat_dong: sql += " WHERE HoatDong = ?" tham_so = (1,) sql += " ORDER BY MaKH" with ket_noi() as conn: return [dict(d) for d in conn.execute(sql, tham_so).fetchall()]def tim_theo_ma(ma): """Tìm khách hàng theo mã, trả về None nếu không có.""" with ket_noi() as conn: dong = conn.execute( "SELECT * FROM KhachHang WHERE MaKH = ?", (ma.strip().upper(),) ).fetchone() return dict(dong) if dong else Nonedef tim_theo_ten(tu_khoa): """Tìm khách hàng theo tên (tương đương LIKE '%...%').""" with ket_noi() as conn: rows = conn.execute( "SELECT * FROM KhachHang WHERE TenKH LIKE ? ORDER BY TenKH", (f"%{tu_khoa.strip()}%",) ).fetchall() return [dict(r) for r in rows]def cap_nhat_cong_no(ma, so_tien_thay_doi): """Cộng/trừ công nợ. Ném ValueError nếu KH không tồn tại.""" kh = tim_theo_ma(ma) if kh is None: raise ValueError(f"Khong tim thay khach hang '{ma}'") moi = Decimal(kh["CongNo"]) + Decimal(str(so_tien_thay_doi)) if moi < 0: raise ValueError(f"Cong no khong the am (se thanh {moi:,})") with ket_noi() as conn: conn.execute("UPDATE KhachHang SET CongNo = ? WHERE MaKH = ?", (str(moi), ma.strip().upper())) return moidef thong_ke_theo_tinh(): """Tổng hợp số lượng và công nợ theo tỉnh.""" with ket_noi() as conn: rows = conn.execute(""" SELECT Tinh, COUNT(*) AS SoLuong, SUM(CAST(CongNo AS REAL)) AS TongNo FROM KhachHang WHERE HoatDong = 1 GROUP BY Tinh ORDER BY TongNo DESC """).fetchall() return [dict(r) for r in rows]# ==================== TẦNG HIỂN THỊ ====================def in_danh_sach(ds): """In bảng danh sách khách hàng.""" if not ds: print(" (Khong co du lieu)") return print(f"{'MA':<10}{'TEN KHACH HANG':<28}{'TINH':<12}{'CONG NO':>16}{'NGAY TAO':>13}") print("-" * 79) tong = Decimal("0") for kh in ds: no = Decimal(kh["CongNo"]) tong += no print(f"{kh['MaKH']:<10}{kh['TenKH']:<28}{kh['Tinh'] or '':<12}" f"{no:>16,.0f}{kh['NgayTao']:>13}") print("-" * 79) print(f"{'TONG CONG':<50}{tong:>16,.0f}")def in_thong_ke(): """In bảng thống kê theo tỉnh.""" tk = thong_ke_theo_tinh() print(f"{'TINH':<14}{'SO KH':>8}{'TONG CONG NO':>20}") print("-" * 42) for d in tk: print(f"{d['Tinh'] or '(Khong ro)':<14}{d['SoLuong']:>8}{d['TongNo'] or 0:>20,.0f}")# ==================== CHƯƠNG TRÌNH CHÍNH ====================def nap_du_lieu_mau(): """Nạp dữ liệu mẫu nếu database đang rỗng.""" if lay_tat_ca(chi_hoat_dong=False): return mau = [ ("KH001", "Cong ty TNHH An Phat", "Hue", Decimal("45800000")), ("KH002", "Cong ty CP Binh Minh", "TP HCM", Decimal("12500000")), ("KH003", "DNTN Cuong Thinh", "Hue", Decimal("0")), ("KH004", "Cong ty TNHH Dai Loi", "Ha Noi", Decimal("89200000")), ("KH005", "HTX Nong nghiep E", "Hue", Decimal("3200000")), ] for ma, ten, tinh, no in mau: them_khach_hang(ma, ten, tinh, no) print("Da nap du lieu mau\n")def menu(): """Vòng lặp menu chính.""" while True: print() print("=" * 45) print(" QUAN LY KHACH HANG") print("=" * 45) print(" 1. Xem danh sach") print(" 2. Tim theo ma") print(" 3. Tim theo ten") print(" 4. Them khach hang") print(" 5. Cap nhat cong no") print(" 6. Thong ke theo tinh") print(" 0. Thoat") print("=" * 45) chon = input("Chon chuc nang: ").strip() try: if chon == "1": in_danh_sach(lay_tat_ca()) elif chon == "2": ma = input("Nhap ma KH: ") kh = tim_theo_ma(ma) if kh: in_danh_sach([kh]) else: print("Khong tim thay khach hang nay") elif chon == "3": tu = input("Nhap tu khoa ten: ") in_danh_sach(tim_theo_ten(tu)) elif chon == "4": ma = input("Ma KH: ") ten = input("Ten KH: ") tinh = input("Tinh: ") no = input("Cong no ban dau (Enter = 0): ").strip() or "0" them_khach_hang(ma, ten, tinh, Decimal(no)) print("Da them khach hang") elif chon == "5": ma = input("Ma KH: ") tien = input("So tien thay doi (am = giam): ") moi = cap_nhat_cong_no(ma, tien) print(f"Cong no moi: {moi:,.0f}") elif chon == "6": in_thong_ke() elif chon == "0": print("Tam biet!") break else: print("Lua chon khong hop le") except ValueError as e: print(f"Loi: {e}") except sqlite3.Error as e: print(f"Loi database: {e}")def main(): khoi_tao_db() nap_du_lieu_mau() menu()if __name__ == "__main__": main()
11. Thực hành bắt buộc
Bài 1 — Tạo database
Tạo file hoc_sql.db với bảng SanPham (MaSP, TenSP, DonViTinh, DonGia, SoLuongTon, NgayCapNhat). Chèn 10 sản phẩm.
Bài 2 — CRUD đầy đủ
Với bảng ở bài 1, viết các hàm:
them_san_pham(...)
sua_san_pham(ma, **thay_doi)
xoa_san_pham(ma)
lay_tat_ca()
tim_theo_ma(ma)
Tất cả dùng parameterized query.
Bài 3 — Truy vấn tổng hợp
Viết các hàm truy vấn trả về:
Tổng giá trị tồn kho
5 sản phẩm giá trị tồn cao nhất
Sản phẩm sắp hết hàng (tồn < 10)
Số sản phẩm theo từng đơn vị tính (GROUP BY)
Giá trị tồn trung bình
Bài 4 — SQL Injection
Viết một hàm tìm kiếm dùng nối chuỗi (cách sai)
Thử nhập ' OR '1'='1 để lấy toàn bộ dữ liệu
Thử nhập '; DROP TABLE SanPham; -- (làm trên database sao lưu!)
Viết lại bằng parameterized query
Thử lại các input trên, xác nhận đã an toàn
Ghi kết luận vào nhật ký
Bài 5 — Transaction
Viết hàm chuyen_kho(ma_sp, tu_kho, den_kho, so_luong) thực hiện 2 lệnh UPDATE trong một transaction. Cố tình gây lỗi ở lệnh thứ 2, xác nhận rollback hoạt động.