Schema đã khai báo đơn hàng tham chiếu khách hàng, nhưng một script vẫn ghi được customer_id không tồn tại. Trước khi sửa toàn bộ lớp truy cập dữ liệu, hãy kiểm tra connection đó có thực sự bật kiểm tra khóa ngoại hay chưa.
Bài này tập trung SQLite, không suy rộng cách cấu hình sang PostgreSQL. Ví dụ đã chạy bằng Python 3.9.6 với SQLite runtime 3.54.0, đối chiếu tài liệu ngày 31/10/2026. Mỗi database thử nằm trong RAM, không mở database của ứng dụng.
1. Đọc lại trạng thái trên đúng connection
PRAGMA foreign_keys trả trạng thái thực thi constraint trên connection hiện tại. Không suy ra trạng thái của toàn bộ ứng dụng từ một tab console khác. Connection mới hoặc pool khác cần được khởi tạo theo chính sách của dự án.
SQLite cho phép cấu hình mặc định khi biên dịch, nên cách ổn định là chủ động đặt giá trị mong muốn và đọc lại. Nếu truy vấn không trả hàng 0 hoặc 1, kiểm tra runtime có hỗ trợ foreign key thay vì coi thiếu kết quả là đã bật.
PRAGMA foreign_keys = ON;
PRAGMA foreign_keys;
Thực hiện bước khởi tạo trước khi bắt đầu transaction. Theo tài liệu SQLite, đổi giá trị khi đang có transaction không bật hoặc tắt enforcement như bạn có thể kỳ vọng; thao tác đó không có hiệu lực trong transaction đang mở.
2. Một phép thử có đối chứng
Đoạn Python sau cố tình tạo hai connection với chế độ khác nhau. Hai database có cùng schema nhưng độc lập, để kết quả không phụ thuộc một file local đã có dữ liệu. isolation_level=None giữ ví dụ trong chế độ autocommit, tránh một transaction ngầm che mất thời điểm đặt PRAGMA.
import sqlite3
def demo(enabled):
db = sqlite3.connect(":memory:", isolation_level=None)
try:
db.execute("PRAGMA foreign_keys = " + ("ON" if enabled else "OFF"))
actual = db.execute("PRAGMA foreign_keys").fetchone()
assert actual == (int(enabled),)
db.executescript("""
CREATE TABLE customers(id INTEGER PRIMARY KEY);
CREATE TABLE orders(
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id)
);
INSERT INTO customers VALUES (1);
INSERT INTO orders VALUES (10, 1);
""")
rejected = False
try:
db.execute("INSERT INTO orders VALUES (?, ?)", (11, 999))
except sqlite3.IntegrityError:
rejected = True
count = db.execute("SELECT COUNT(*) FROM orders").fetchone()[0]
violations = db.execute("PRAGMA foreign_key_check").fetchall()
assert rejected == enabled
assert count == (1 if enabled else 2)
assert len(violations) == (0 if enabled else 1)
print(enabled, rejected, count, len(violations))
finally:
db.close()
print("SQLite", sqlite3.sqlite_version)
demo(False)
demo(True)
Hai dòng kết quả của fixture là False False 2 1 và True True 1 0. Khi tắt kiểm tra, đơn 11 được ghi và phép kiểm tra tìm được một vi phạm. Khi bật, insert sai bị từ chối, đơn hợp lệ vẫn còn.
Đây là lỗi được tạo có chủ đích trong database tạm. Không tắt constraint trên dữ liệu thật để xem một insert sai có lọt hay không. Có thể đưa phép thử tương đương vào bộ kiểm thử tạo connection của ứng dụng.
3. Bật enforcement không sửa dữ liệu cũ
Giả sử dữ liệu mồ côi đã được ghi trong quá khứ. Đặt ON chỉ thay đổi cách kiểm tra các thao tác tiếp theo; đó không phải lệnh sửa các dòng đã sai. PRAGMA foreign_key_check giúp liệt kê vi phạm để bạn lập kế hoạch xử lý.
Không tự xóa tất cả các hàng được trả về. Một customer_id sai có thể cần khôi phục khách hàng, sửa liên kết hoặc xóa đơn theo quyết định nghiệp vụ. Ghi lại khóa và đối chiếu nguồn dữ liệu trước khi lựa chọn, rồi chạy lại kiểm tra sau thao tác đã được duyệt.
4. Đặt kiểm tra ở nơi tạo connection
Nếu dự án dùng pool, tìm hook khởi tạo connection của driver hoặc ORM đang dùng. Thực hiện PRAGMA và đọc lại ở đó, trước transaction nghiệp vụ. Không đặt một lần trong migration rồi cho rằng mọi worker hoặc script chạy sau sẽ kế thừa.
Trong ví dụ, cột tham chiếu có NOT NULL vì mỗi đơn phải có khách hàng. Foreign key tự nó không bắt buộc giá trị khác NULL. Quy tắc dữ liệu bắt buộc và quy tắc tồn tại ở bảng cha là hai điều cần khai báo rõ trong schema.
Kiểm tra và giới hạn
Thử đường đi hợp lệ, insert tham chiếu sai, cùng một script quản trị mở connection mới. Với ứng dụng có transaction, kiểm tra thời điểm PRAGMA thay vì chỉ đọc log khởi động. Các nhánh ON/OFF trong bài đã chạy thật, nhưng chưa kiểm thử pool hoặc ORM cụ thể.
Khi thấy constraint không hoạt động, kiểm tra theo thứ tự: đúng runtime, đúng connection, đúng thời điểm và đúng schema. Cách này giúp tìm nơi bỏ sót enforcement trước khi thêm các truy vấn kiểm tra thủ công ở mọi chỗ ghi dữ liệu.


