Menu

Ràng buộc CHECK trong SQLite: kiểm tra dữ liệu ngay ở bảng

Cách dùng ràng buộc CHECK trong SQLite để áp quy tắc lên giá trị của cột: kiểm tra một cột, kiểm tra nhiều cột, ràng buộc có tên và những điểm dễ mắc lỗi.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Ràng buộc CHECK là quy tắc mà mọi hàng phải thỏa mãn

Ràng buộc CHECK là một biểu thức boolean bạn gắn vào bảng. SQLite đánh giá nó ở mỗi lệnh INSERT và UPDATE, và nếu biểu thức trả về sai, thao tác sẽ thất bại. Đây là cách đưa thẳng một quy tắc nghiệp vụ, "giá không được âm", "trạng thái phải là một trong ba giá trị này", vào schema.

Hai hàng đầu tiên được chèn vào. Hàng thứ ba gây ra lỗi CHECK constraint failed và bị từ chối: bảng không bao giờ nhận nó. Ràng buộc áp quy tắc cho mọi nơi ghi dữ liệu, dù đó là ứng dụng của bạn, một script migration hay ai đó đang mày mò trong CLI.

Cấp cột và cấp bảng

Bạn có thể viết CHECK ở hai chỗ: sau định nghĩa một cột (cấp cột) hoặc sau tất cả các cột (cấp bảng). Chúng hoạt động như nhau; khác biệt nằm ở cách nào đọc tự nhiên hơn.

Lượt đặt chỗ đầu tiên được chèn vào. Lượt thứ hai thất bại: thời điểm kết thúc nằm trước thời điểm bắt đầu. Quy tắc trên một cột đọc dễ hơn khi viết ở cấp cột; quy tắc nào so sánh từ hai cột trở lên đọc dễ hơn khi viết ở cấp bảng.

Giới hạn giá trị trong một danh sách

Một cách dùng phổ biến là buộc một cột chỉ nhận một trong số các giá trị cố định. SQLite không có kiểu enum gốc, nên CHECK ... IN (...) là cách viết quen thuộc:

Hàng thứ ba thất bại: 'pending' không có trong danh sách cho phép. Nếu sau này cần thêm một trạng thái mới, bạn sẽ phải dựng lại bảng (nói thêm ở bên dưới), nên hãy cân nhắc một chút trước khi khóa cứng danh sách. Nhưng với những bộ từ vựng thật sự cố định như tên vai trò hay trạng thái đơn hàng, đây chính xác là ràng buộc bạn cần.

Đặt tên cho ràng buộc

Mặc định, ràng buộc không có tên. Thông báo lỗi chỉ ghi "CHECK constraint failed" kèm biểu thức, vẫn ổn khi bảng có một CHECK, nhưng sẽ khó hiểu khi có năm CHECK. Thêm tên bằng CONSTRAINT:

Giờ thông báo lỗi có kèm tên ràng buộc, nên bạn biết ngay quy tắc nào bị vi phạm. Đặt tên tốn thêm vài ký tự và đáng giá ngay lần đầu tiên có gì đó hỏng trên production.

CHECK và NULL: điểm dễ mắc lỗi

CHECK cho qua khi biểu thức đúng hoặc là NULL. Nó chỉ thất bại khi biểu thức sai một cách tường minh. Nghe có vẻ lạ, cho đến khi bạn nhớ rằng hầu như mọi phép so sánh với NULL đều cho ra NULL, chứ không phải đúng hay sai.

Hàng NULL được chèn vào trơn tru: NULL >= 0 là NULL, không phải sai, nên CHECK không thất bại. Nếu bạn thật sự muốn cấm cả số âm lẫn giá trị bị thiếu, hãy kết hợp NOT NULL với CHECK:

Giờ lệnh chèn thất bại ở ràng buộc NOT NULL trước cả khi CHECK chạy. Hai ràng buộc phối hợp với nhau: NOT NULL lo chuyện thiếu giá trị, CHECK lo chuyện giá trị có đúng dạng hay không.

Các hàm có sẵn hữu ích bên trong CHECK

Biểu thức có thể dùng phần lớn các hàm có sẵn của SQLite. Vài hàm hay gặp:

Ba lỗi: một email sai định dạng, một tên người dùng quá ngắn và một mã quốc gia viết thường. LIKE xử lý các mẫu đơn giản; length(), upper(), lower() và các phép tính số học đều dùng được. Chỉ cần giữ biểu thức có tính xác định (deterministic): dùng thứ như random() hay current_timestamp bên trong CHECK tạo ra những quy tắc có thể thay đổi giữa các hàng, hiếm khi là điều bạn muốn.

CHECK so với trigger

CHECK và trigger đều có thể từ chối dữ liệu sai, và người mới thường thắc mắc nên dùng cái nào. Quy tắc chung:

  • CHECK khi quy tắc chỉ phụ thuộc vào hàng đang được ghi. "Cột này so với cột kia", "giá trị này nằm trong một khoảng", "chuỗi này khớp một mẫu".
  • Trigger (cụ thể là trigger BEFORE INSERT/UPDATE gọi RAISE) khi quy tắc phụ thuộc vào các hàng khác, các bảng khác, hoặc cần làm việc gì đó phức tạp hơn một biểu thức boolean đơn lẻ.

CHECK nhanh hơn, đơn giản hơn và hiển thị ngay trong schema: ai đọc CREATE TABLE cũng thấy quy tắc. Chỉ dùng trigger khi CHECK không diễn đạt được điều bạn cần.

Không thể xóa CHECK bằng ALTER

Đây là điểm bất tiện duy nhất. SQLite không có ALTER TABLE ... DROP CONSTRAINT. Để xóa hoặc đổi một CHECK, bạn phải dựng lại bảng:

BEGIN;

CREATE TABLE products_new (
    id    INTEGER PRIMARY KEY,
    name  TEXT NOT NULL,
    price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);

INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;

COMMIT;

Hãy bọc toàn bộ trong một transaction để nếu thất bại giữa chừng, cơ sở dữ liệu vẫn được giữ nguyên. Nếu các bảng khác có khóa ngoại trỏ đến bảng bạn đang dựng lại, các bước sẽ dài hơn: tắt foreign_keys, dựng lại, bật lại, kiểm tra lại. Chúng ta sẽ nói về điều đó trong tài liệu về migration ở phần sau của chương trình học.

Tiếp theo: ràng buộc UNIQUE

CHECK kiểm tra hình dạng của các giá trị bên trong một hàng. Ràng buộc tiếp theo, UNIQUE, kiểm tra mối quan hệ giữa các hàng: đảm bảo không có hai hàng nào trùng giá trị trong một cột hoặc một nhóm cột. Đó là nội dung tiếp theo.

Câu hỏi thường gặp

Ràng buộc CHECK trong SQLite là gì?

Ràng buộc CHECK là một biểu thức boolean gắn với bảng mà mọi hàng đều phải thỏa mãn. SQLite đánh giá nó ở mỗi lệnh INSERT hoặc UPDATE, và từ chối thay đổi nếu biểu thức sai. Đây là cách đơn giản nhất để áp một quy tắc như 'giá phải là số dương' mà không cần viết code ứng dụng.

Ràng buộc CHECK trong SQLite có tham chiếu nhiều cột được không?

Được. Hãy viết nó thành ràng buộc cấp bảng thay vì gắn vào một cột. Ví dụ, CHECK (start_date <= end_date) khai báo sau danh sách cột có thể tham chiếu cả hai cột. Về mặt kỹ thuật, CHECK cấp cột cũng có thể tham chiếu cột khác, nhưng cấp bảng dễ đọc hơn khi liên quan đến nhiều hơn một cột.

Vì sao ràng buộc CHECK trong SQLite không chặn NULL?

CHECK cho qua khi biểu thức đúng hoặc là NULL: nó chỉ thất bại khi biểu thức sai một cách tường minh. Vì vậy CHECK (age >= 0) chấp nhận age là NULL, vì NULL >= 0 là NULL, không phải sai. Nếu bạn cũng muốn cấm NULL, hãy thêm ràng buộc NOT NULL bên cạnh CHECK.

Có thể xóa hoặc sửa ràng buộc CHECK trong SQLite không?

Không trực tiếp. SQLite không hỗ trợ ALTER TABLE ... DROP CONSTRAINT. Để đổi một CHECK, bạn hoặc sửa sqlite_schema bằng PRAGMA writable_schema (nâng cao, rủi ro) hoặc dựng lại bảng: tạo bảng mới với các ràng buộc mong muốn, sao chép dữ liệu sang, xóa bảng cũ và đổi tên. Đặt tên cho các ràng buộc giúp script dựng lại bảng dễ đọc hơn.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu