Menu

NOT NULL và DEFAULT trong SQLite: ràng buộc cho cột

Cách NOT NULL và DEFAULT hoạt động trong SQLite: chúng thật sự bắt buộc điều gì, mẹo CURRENT_TIMESTAMP và những điểm cần lưu ý khi thêm chúng vào bảng đã có.

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.

Hai ràng buộc rất đáng dùng

Phần lớn bug sinh ra từ một schema cẩu thả đều bắt nguồn từ một trong hai điều: một cột mang NULL khi không ai ngờ tới, hoặc một cột thiếu giá trị mà ứng dụng mặc định là có. NOT NULL và DEFAULT giải quyết cả hai, và gần như không tốn gì để thêm vào.

Một cột là bắt buộc và không có giá trị dự phòng. Hai cột có giá trị dự phòng. Lệnh insert chỉ cần cung cấp email, SQLite tự điền phần còn lại. Toàn bộ tính năng nằm gọn trong một ví dụ; phần còn lại của trang nói về các trường hợp biên.

NOT NULL nghĩa là "từ chối NULL, không ngoại lệ"

NOT NULL làm đúng như tên gọi. Mọi cố gắng đưa NULL vào cột, dù bằng cách bỏ cột khỏi lệnh INSERT khi không có giá trị mặc định, hay bằng cách viết NULL tường minh, đều thất bại:

Thông báo lỗi là:

Runtime error: NOT NULL constraint failed: posts.title

Kết quả tương tự nếu bạn truyền NULL trực tiếp:

INSERT INTO posts (id, title) VALUES (1, NULL);
-- Runtime error: NOT NULL constraint failed: posts.title

Đó là cam kết. Nếu về mặt logic một cột là bắt buộc, hãy đánh dấu NOT NULL và bạn đã loại bỏ cả một nhóm bug: không đoạn code ứng dụng nào lén đưa được NULL qua cơ sở dữ liệu.

DEFAULT cung cấp giá trị khi người gọi không cung cấp

DEFAULT chỉ có tác dụng khi lệnh INSERT hoàn toàn không nhắc đến cột. Nó không cứu được một NULL tường minh:

Lệnh insert đầu tiên dựa vào giá trị mặc định. Lệnh thứ hai ghi đè nó. Nếu bạn viết INSERT INTO tasks (title, status) VALUES ('x', NULL), bạn sẽ nhận lỗi NOT NULL constraint failed: cột đã được nêu tên, nên giá trị mặc định không bao giờ được dùng.

Đây là cách hình dung nên ghi nhớ: DEFAULT điền vào các cột bị thiếu. NOT NULL từ chối null dù nó đến bằng cách nào. Đó là hai tính năng độc lập và kết hợp với nhau rất tốt.

Giá trị mặc định có thể là biểu thức

Giá trị mặc định là hằng là trường hợp phổ biến (DEFAULT 0, DEFAULT '', DEFAULT 'pending'), nhưng SQLite cũng chấp nhận một biểu thức trong ngoặc. Đây là cách đóng dấu thời điểm tạo cho từng hàng, hoặc sinh một ID ngẫu nhiên:

Vài điều cần biết:

  • Biểu thức được tính ở mỗi lần insert, không phải một lần lúc tạo bảng. Mỗi hàng có dấu thời gian riêng và token riêng.
  • CURRENT_TIMESTAMP, CURRENT_DATE và CURRENT_TIME là ba từ khóa đặc biệt không cần ngoặc. Mọi biểu thức khác đều cần.
  • Biểu thức không được tham chiếu tới cột khác hay truy vấn con: nó phải tự đầy đủ.

Nếu muốn một cột là tùy chọn nhưng được tự đóng dấu khi có mặt, hãy bỏ NOT NULL và giữ giá trị mặc định. Nếu muốn nó vừa bắt buộc vừa được tự đóng dấu, hãy dùng cả hai.

DEFAULT NULL là hợp lệ (và đôi khi chính là mục đích)

Viết DEFAULT NULL cũng giống như không có giá trị mặc định nào: cột sẽ là NULL khi bạn không cung cấp giá trị. Nên dùng nó khi bạn muốn nói rõ trong schema rằng "không có giá trị" là trạng thái ban đầu có chủ đích:

Ở đây bio và avatar hoạt động giống hệt nhau. DEFAULT NULL trên bio là một lời chú thích dưới dạng code: nó cho người đọc schema biết rằng việc chưa có bio là trạng thái bình thường, không phải do sơ suất.

Thêm NOT NULL vào bảng đã có

Đây là chỗ mọi thứ bắt đầu rắc rối. ALTER TABLE của SQLite được giới hạn có chủ đích: bạn không thể chạy ALTER COLUMN ... SET NOT NULL như trong Postgres. Những gì bạn có thể làm phụ thuộc vào việc cột đã tồn tại hay chưa.

Với một cột hoàn toàn mới, ADD COLUMN ... NOT NULL chạy được, nhưng bạn bắt buộc phải cung cấp giá trị mặc định; nếu không, các hàng đã có sẽ bỗng dưng chứa NULL trong một cột NOT NULL, điều không thể xảy ra:

Thử làm tương tự mà không có giá trị mặc định, bạn sẽ nhận lỗi:

ALTER TABLE products ADD COLUMN sku TEXT NOT NULL;
-- Runtime error: Cannot add a NOT NULL column with default value NULL

Với một cột đã có, không có cách thay đổi tại chỗ. Công thức chuẩn là dựng lại bảng: tạo bảng mới với ràng buộc bạn muốn, sao chép dữ liệu sang, xóa bảng cũ, đổi tên bảng mới. Trang drop-and-alter-table sẽ nói chi tiết về việc này; giờ bạn chỉ cần biết giới hạn này là có thật và hãy tính đến nó khi thiết kế schema.

Một ví dụ kết hợp thực tế

Hầu hết các bảng production dùng cả hai ràng buộc cùng lúc để mô tả "những gì ứng dụng kỳ vọng là đúng":

Đọc schema đó từ trên xuống, bạn có thể đoán ứng dụng làm gì mà chưa cần xem dòng code nào. customer là bắt buộc và không có giá trị dự phòng: người gọi phải biết đơn hàng này của ai. Số tiền, đơn vị tiền tệ và trạng thái đều có giá trị mặc định hợp lý, nên lệnh insert đơn giản nhất vẫn tạo ra một hàng nhất quán. notes là tùy chọn. created_at do cơ sở dữ liệu điền, và đó là nơi duy nhất nên điền nó.

Đó là giá trị của các ràng buộc này: chúng biến những giả định thành quy tắc mà chính cơ sở dữ liệu bắt buộc tuân theo.

Những lỗi thường gặp

Danh sách ngắn những điều hay khiến người dùng vấp:

  • NULL tường minh vô hiệu hóa DEFAULT. INSERT INTO t (col) VALUES (NULL) sẽ không dùng giá trị mặc định. Cột phải vắng mặt trong danh sách cột.
  • Biểu thức mặc định cần ngoặc. DEFAULT CURRENT_TIMESTAMP chạy được (đó là một trong ba từ khóa đặc biệt). DEFAULT lower(hex(randomblob(8))) thì không: hãy bọc lại thành DEFAULT (lower(hex(randomblob(8)))).
  • NOT NULL và chuỗi rỗng là hai chuyện khác nhau. '' là một giá trị TEXT hợp lệ và không vi phạm ràng buộc. Nếu cũng muốn cấm chuỗi rỗng, đó là việc của CHECK (trang tiếp theo).
  • ADD COLUMN ... NOT NULL đòi hỏi một DEFAULT khác NULL. Không có nó, SQLite từ chối thay đổi.

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

NOT NULL và DEFAULT lo phần "phải có" và "điền vào nếu thiếu". Với lớp kiểm tra tiếp theo, như "phải là số dương", "phải là một trong các giá trị này", "ngày kết thúc phải sau ngày bắt đầu", SQLite có ràng buộc CHECK, cho phép bạn viết biểu thức boolean tùy ý mà mọi hàng đều phải thỏa mãn. Đó là nội dung của trang tiếp theo.

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

Làm sao bắt buộc một cột phải có giá trị trong SQLite?

Thêm NOT NULL vào định nghĩa cột: email TEXT NOT NULL. Mọi lệnh INSERT hoặc UPDATE cố để cột đó là NULL đều thất bại với lỗi NOT NULL constraint failed. Kết hợp thêm DEFAULT nếu bạn muốn có giá trị dự phòng khi người gọi không cung cấp.

Giá trị mặc định trong SQLite hoạt động thế nào?

DEFAULT <value> cho cột một giá trị để dùng khi lệnh INSERT không chỉ định. Giá trị mặc định có thể là một hằng (DEFAULT 0, DEFAULT 'pending'), NULL, hoặc một biểu thức trong ngoặc như DEFAULT (CURRENT_TIMESTAMP) hay DEFAULT (lower(hex(randomblob(8)))). Biểu thức mặc định được tính lại ở mỗi lần insert.

Vì sao SQLite báo 'NOT NULL constraint failed' khi insert?

Bạn đang thêm một hàng mà không cung cấp giá trị cho một cột NOT NULL không có DEFAULT. Hãy đưa cột đó vào lệnh INSERT, đặt DEFAULT cho cột, hoặc nới lỏng ràng buộc. Truyền NULL một cách tường minh cũng gây ra lỗi này: NOT NULL từ chối null bất kể nó đến từ đâu.

Có thể thêm NOT NULL cho một cột đã có trong SQLite không?

Không trực tiếp: SQLite không có ALTER TABLE ... ALTER COLUMN. Bạn hoặc thêm cột mới với NOT NULL DEFAULT <value> (giá trị mặc định là bắt buộc cho các hàng đã có), hoặc dựng lại bảng: tạo bảng mới có ràng buộc, sao chép dữ liệu sang, xóa bảng cũ rồi đổi tên bảng mới.

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