Tại sao không nên phó mặc hoàn toàn cho Backend?
Nếu từng dùng MySQL bản cũ (như 5.7), chắc hẳn bạn đã trải qua cảm giác “bị lừa” khi viết câu lệnh CHECK nhưng database vẫn thản nhiên nhận dữ liệu sai. Trước bản 8.0.16, MySQL chỉ nhận cú pháp cho có chứ không hề thực thi (enforce). Mọi logic như “giá phải dương” hay “tuổi từ 18” đều bị đẩy hết lên tầng Application.
Tin tưởng tuyệt đối vào Backend là một sai lầm đắt giá. Tôi từng xử lý một sự cố tại sàn TMĐT, nơi nhân viên nhập nhầm giá sản phẩm thành số âm qua tool quản lý trực tiếp. Do không có ràng buộc ở database, hệ thống vẫn nhận giá -100k, dẫn đến hàng trăm đơn hàng bị lỗi chỉ trong 10 phút. Check Constraints sinh ra để làm lớp lá chắn cuối cùng, chặn đứng mọi dữ liệu rác dù chúng đến từ code, script import hay thao tác tay.
Khi định nghĩa quy tắc ngay tại cấu trúc bảng, MySQL sẽ từ chối thẳng thừng các lệnh INSERT hoặc UPDATE vi phạm. Điều này giúp dữ liệu luôn sạch và nhất quán, giảm bớt gánh nặng validation cho anh em làm Backend.
Triển khai cơ bản: Đừng quên đặt tên Constraint
Điều kiện tiên quyết: Bạn phải chạy MySQL 8.0.16 trở lên. Các bản cũ hơn sẽ lặng lẽ bỏ qua ràng buộc mà không hề cảnh báo, cực kỳ nguy hiểm cho hệ thống của bạn.
Dưới đây là cách tôi thường thiết lập bảng sản phẩm để đảm bảo an toàn:
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2),
stock_quantity INT,
-- Ràng buộc giá phải lớn hơn 0
CONSTRAINT chk_price_positive CHECK (price > 0),
-- Ràng buộc tồn kho không được âm
CONSTRAINT chk_stock_min CHECK (stock_quantity >= 0)
);
Một lưu ý nhỏ nhưng cực quan trọng: Hãy luôn đặt tên tường minh cho constraint (như chk_price_positive). Nếu để MySQL tự sinh tên kiểu products_chk_1, bạn sẽ cực kỳ vất vả khi cần debug hoặc muốn xóa nó sau này.
Thử chèn một bản ghi sai quy tắc:
INSERT INTO products(product_name, price, stock_quantity)
VALUES ('iPhone 15', -100, 10);
MySQL sẽ chặn lại ngay với thông báo: Check constraint 'chk_price_positive' is violated.
3 kịch bản ứng dụng thực tế
Trong dự án thực tế, logic thường lắt léo hơn việc so sánh lớn hơn/nhỏ hơn đơn thuần. Dưới đây là 3 cách vận dụng linh hoạt hơn.
1. Ràng buộc giữa các cột (Multi-column)
Với bảng khuyến mãi, ngày kết thúc bắt buộc phải sau ngày bắt đầu. Check Constraint xử lý việc này cực gọn:
CREATE TABLE promotions (
id INT AUTO_INCREMENT PRIMARY KEY,
start_date DATE,
end_date DATE,
CONSTRAINT chk_promo_dates CHECK (end_date >= start_date)
);
2. Thay thế ENUM bằng toán tử IN
Kiểu ENUM trong MySQL đôi khi khá cứng nhắc khi cần thay đổi. Tôi thường dùng VARCHAR kết hợp Check Constraint để linh hoạt hơn:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
status VARCHAR(20),
CONSTRAINT chk_order_status CHECK (status IN ('pending', 'processing', 'shipped', 'cancelled'))
);
3. Áp dụng cho bảng đã có dữ liệu
Khi bảng đã lớn, ví dụ bảng users có vài triệu dòng, việc thêm ràng buộc cần thận trọng. Bạn sử dụng ALTER TABLE để bổ sung luật mới:
ALTER TABLE users
ADD CONSTRAINT chk_user_age CHECK (age >= 18);
Cảnh báo: Nếu bảng đang tồn tại dù chỉ một dòng dữ liệu vi phạm (user 15 tuổi), câu lệnh trên sẽ thất bại ngay lập tức. Bạn phải dọn dẹp dữ liệu rác trước khi áp dụng luật.
Hiệu năng và những giới hạn cần biết
Nhiều người e ngại Check Constraints làm chậm hệ thống. Thực tế, ảnh hưởng này cực kỳ nhỏ, thường dưới 1% và tối ưu hơn nhiều so với việc dùng Trigger (có thể gây overhead tới 20-30%). Việc kiểm tra tại tầng engine giúp phản hồi lỗi nhanh nhất có thể.
Tuy nhiên, công cụ này không phải vạn năng. Bạn cần ghi nhớ 3 hạn chế lớn:
- Không dùng được các hàm không xác định (non-deterministic) như
NOW()hayCURRENT_TIMESTAMP(). - Không thể tham chiếu đến cột của bảng khác (hãy dùng Foreign Key).
- Không cho phép sử dụng Subquery trong biểu thức kiểm tra.
Lời khuyên từ kinh nghiệm của tôi: Hãy ưu tiên Check Constraint cho các quy tắc bất biến như giá cả, số lượng hoặc trạng thái cố định. Với các logic nghiệp vụ thay đổi theo mùa vụ, hãy giữ chúng ở tầng Application để tránh việc phải thay đổi Schema database quá thường xuyên.

