Bạn muốn lấy đơn đang mở hoặc đang chờ của tenant hiện tại. Câu WHERE có đủ ba điều kiện, parameter cũng được bind, nhưng kết quả vẫn xuất hiện đơn của tenant khác. Nguyên nhân có thể nằm ở cách AND và OR được nhóm, không nằm ở giá trị truyền vào.
Bài đọc tài liệu PostgreSQL 17 và SQLite ngày 05/11/2026. Ví dụ được chạy với Python 3.9.6, SQLite 3.54.0 trong database bộ nhớ; chưa chạy PostgreSQL server, ORM hoặc đo hiệu năng. Các tenant và đơn đều là dữ liệu tự tạo phục vụ kiểm tra.
Đọc biểu thức theo thứ tự ưu tiên
Trong hai hệ quản trị được đối chiếu, AND có độ ưu tiên cao hơn OR. Vì thế A AND B OR C được nhóm thành (A AND B) OR C. Nó không có nghĩa là A AND (B OR C).
Gọi A là đúng tenant, B là trạng thái open và C là trạng thái pending. Ở biểu thức đầu tiên, chỉ cần C đúng thì hàng có thể được chọn dù A sai. Xuống dòng hoặc thụt lề không thay thế dấu ngoặc trong việc xác định nhóm biểu thức.
1. Tạo dữ liệu khiến lỗi lộ ra
Chạy đoạn Python sau trong file mới. Database chỉ tồn tại trong bộ nhớ của tiến trình:
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE demo_orders (
id INTEGER PRIMARY KEY,
tenant_id INTEGER NOT NULL,
status TEXT NOT NULL
);
INSERT INTO demo_orders VALUES
(1, 7, 'open'),
(2, 7, 'pending'),
(3, 8, 'pending'),
(4, 7, 'closed'),
(5, 8, 'open');
""")
wrong = db.execute("""
SELECT id FROM demo_orders
WHERE tenant_id = ? AND status = ? OR status = ?
ORDER BY id
""", (7, "open", "pending")).fetchall()
correct = db.execute("""
SELECT id FROM demo_orders
WHERE tenant_id = ? AND (status = ? OR status = ?)
ORDER BY id
""", (7, "open", "pending")).fetchall()
assert wrong == [(1,), (2,), (3,)]
assert correct == [(1,), (2,)]
print("wrong:", wrong)
print("correct:", correct)
Hàng 3 thuộc tenant 8 nhưng có trạng thái pending. Nó là ca đối chứng quyết định: nếu bộ test chỉ chứa dữ liệu tenant 7, hai truy vấn có thể trả kết quả giống nhau và lỗi không lộ.
Binding giữ giá trị tách khỏi cú pháp SQL; nó không tự sửa cấu trúc logic của câu lệnh. Ví dụ dùng placeholder ? theo sqlite3 của Python. Khi chuyển sang driver PostgreSQL, dùng cú pháp parameter của driver, không thay bằng nối chuỗi.
2. Dùng IN khi hai trạng thái áp lên cùng một cột
Trong trường hợp này có thể diễn đạt gọn bằng IN. Nối tiếp file vừa chạy:
with_in = db.execute("""
SELECT id FROM demo_orders
WHERE tenant_id = ? AND status IN (?, ?)
ORDER BY id
""", (7, "open", "pending")).fetchall()
assert with_in == correct
other_tenant = db.execute("""
SELECT id FROM demo_orders
WHERE tenant_id = ? AND status IN (?, ?)
ORDER BY id
""", (8, "open", "pending")).fetchall()
assert other_tenant == [(3,), (5,)]
db.close()
IN ở đây giúp người đọc nhìn thấy một nhóm trạng thái. Đừng dùng nó để thay cơ học mọi OR: hai nhánh có thể liên quan các cột hoặc quy tắc hoàn toàn khác nhau. Với danh sách động, tạo đúng số placeholder và bind từng giá trị; xác định riêng hành vi khi danh sách rỗng.
Kiểm tra query builder và các điều kiện bổ sung
Khi dùng ORM, đọc SQL sinh ra thay vì chỉ nhìn chuỗi method trong code. Nhóm OR thường cần một callback hoặc biểu thức con của thư viện. Kiểm tra cả tenant hiện tại, tenant khác có cùng trạng thái, trạng thái bị loại và tenant không có hàng.
Nếu thêm điều kiện chưa xóa hoặc quyền xem, đặt nó ở cấp nhóm đúng với yêu cầu. Viết câu nghiệp vụ bằng lời trước: tất cả hàng phải thuộc tenant hiện tại, và trạng thái phải nằm trong danh sách được chọn. Sau đó đối chiếu từng nhóm ngoặc với câu ấy.
Giới hạn cần giữ rõ
Schema demo đặt tenant và status NOT NULL nên không đi vào logic ba giá trị của NULL. Thêm cột nullable cần thêm ca kiểm tra riêng. Bài cũng không khẳng định thứ tự thực thi vật lý của điều kiện; ưu tiên toán tử quyết định cách hiểu biểu thức, không buộc optimizer đánh giá từng vế theo thứ tự viết.
Một WHERE đúng là phần cần kiểm tra, chưa thay thế toàn bộ thiết kế phân quyền. Giá trị tenant phải đến từ ngữ cảnh đã được xác thực của ứng dụng. Kết quả kiểm thử nhỏ chứng minh lỗi và cách nhóm trong fixture này; hãy đưa ca tenant chéo vào bộ regression test của endpoint thực tế.


