Menu

Khóa ngoại trong SQLite: REFERENCES, ON DELETE và PRAGMA

Cách khóa ngoại hoạt động trong SQLite: khai báo REFERENCES, bật kiểm tra bằng PRAGMA và chọn hành vi ON DELETE phù hợp.

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.

Khóa ngoại là con trỏ giữa các bảng

Khóa ngoại (foreign key) là một cột trong một bảng mà giá trị của nó phải khớp với một hàng trong bảng khác. Đó là cách cơ sở dữ liệu quan hệ nói "hàng posts này thuộc về hàng authors kia" mà không phải chép tên và email của tác giả vào mọi bài viết.

Đây là ví dụ nhỏ nhất có thể: một bảng cha và một bảng con được liên kết bằng khóa ngoại:

author_id INTEGER REFERENCES authors(id) là toàn bộ khai báo khóa ngoại. Nó nói rằng: cột này chứa một id từ bảng authors. Giờ cơ sở dữ liệu biết hai bảng có liên quan với nhau, và nếu chế độ kiểm tra đang bật, nó sẽ từ chối những lệnh thêm dữ liệu trỏ đến tác giả không tồn tại.

Khóa ngoại mặc định bị tắt

Đây là điều quan trọng nhất về khóa ngoại trong SQLite, và ai cũng bất ngờ: SQLite đọc hiểu mệnh đề REFERENCES nhưng không kiểm tra chúng trừ khi bạn yêu cầu. Lý do là để tương thích với các phiên bản cũ: những cơ sở dữ liệu đời đầu được tạo ra trước khi tính năng này tồn tại.

Xem điều gì xảy ra khi không bật kiểm tra:

Hàng mồ côi đã được thêm vào ngay. Để có được sự bảo vệ bạn thực sự cần, chạy PRAGMA foreign_keys = ON; ở đầu mỗi kết nối:

Giờ lệnh thêm thất bại với lỗi FOREIGN KEY constraint failed. Pragma này áp dụng theo từng kết nối, không theo cơ sở dữ liệu: thiết lập không được lưu trong tệp. Mọi ứng dụng, mọi phiên CLI, mọi fixture kiểm thử đều phải đặt nó. Hầu hết code production chạy PRAGMA foreign_keys = ON; ngay sau khi mở kết nối.

Mệnh đề REFERENCES thực sự đòi hỏi gì

Cột bạn tham chiếu phải là PRIMARY KEY hoặc có ràng buộc UNIQUE. Nhờ vậy SQLite mới đảm bảo được mỗi lần tra cứu đều rõ ràng. Kiểu dữ liệu cũng nên tương thích: SQLite khá dễ dãi về kiểu, nhưng trộn lẫn chúng là tự chuốc lấy bất ngờ.

Bạn có thể viết khóa ngoại theo hai cách. Viết ngay trên cột:

Hoặc viết thành ràng buộc riêng ở cấp bảng, cách bắt buộc khi khóa ngoại gồm nhiều cột:

Cả hai cách đều tạo ra ràng buộc giống hệt nhau. Dùng cách nào dễ đọc hơn với bảng đang viết.

ON DELETE: chuyện gì xảy ra với hàng con

Khi bạn xóa một hàng cha, SQLite phải quyết định làm gì với các hàng con đang trỏ đến nó. Bạn chọn quy tắc bằng ON DELETE:

Xóa Ada cũng xóa luôn cả hai bài viết của cô ấy. Các lựa chọn gồm:

  • CASCADE: xóa luôn các hàng con. Phù hợp với dữ liệu "thuộc sở hữu" như bài viết của một tác giả hay các mặt hàng trong một đơn hàng.
  • SET NULL: gán null cho cột khóa ngoại. Phù hợp khi hàng con vẫn nên tồn tại khi không còn hàng cha (ví dụ bình luận của một người dùng đã bị xóa trở thành ẩn danh).
  • SET DEFAULT: gán cho cột khóa ngoại giá trị mặc định đã khai báo.
  • RESTRICT: chặn lệnh xóa nếu còn hàng con. Thất bại ngay khi câu lệnh chạy.
  • NO ACTION: giá trị mặc định. Trong hầu hết trường hợp hoạt động giống RESTRICT (nó hoãn việc kiểm tra đến lúc commit, nhưng kết quả như nhau: bạn không thể để lại hàng con bị treo).

ON UPDATE hoạt động tương tự khi khóa của hàng cha thay đổi, dù việc cập nhật khóa chính rất hiếm gặp.

Foreign key constraint failed: nghĩa là gì

Bạn sẽ gặp lỗi này trong hai tình huống. Thứ nhất, thêm hoặc cập nhật một hàng con với giá trị không có hàng cha tương ứng:

sqlite> INSERT INTO posts (title, author_id) VALUES ('Stray', 999);
Runtime error: FOREIGN KEY constraint failed

Hoặc tác giả 999 không tồn tại, hoặc bạn đã nhầm kiểu của cột. Hãy thêm hàng cha trước, hoặc sửa lại giá trị.

Thứ hai, xóa (hoặc cập nhật) một hàng cha vẫn còn hàng con, khi khóa ngoại dùng RESTRICT hoặc NO ACTION:

sqlite> DELETE FROM authors WHERE id = 1;
Runtime error: FOREIGN KEY constraint failed

Hãy xóa các hàng con trước, hoặc đổi khóa ngoại sang ON DELETE CASCADE/SET NULL nếu bạn thực sự muốn xóa dây chuyền.

Còn một lỗi họ hàng ít gặp hơn: FOREIGN KEY mismatch. Lỗi này xuất hiện khi cột được tham chiếu không phải khóa chính hay unique, hoặc số lượng cột không khớp. Đây là lỗi cấu trúc bảng, không phải lỗi dữ liệu.

Thêm khóa ngoại cho bảng đã có

ALTER TABLE của SQLite khá hạn chế: bạn có thể thêm một cột có khóa ngoại, nhưng không thể gắn khóa ngoại vào một cột đã tồn tại. Cách làm chuẩn là tạo lại bảng rồi đổi tên:

Các bước: tắt kiểm tra, tạo bảng mới với ràng buộc mong muốn, chép dữ liệu, xóa bảng cũ, đổi tên. BEGIN/COMMIT giúp toàn bộ diễn ra trọn vẹn. Bật lại kiểm tra ở cuối và SQLite sẽ áp dụng ràng buộc mới cho các thao tác sau đó; nhưng nếu có dữ liệu không hợp lệ, transaction đã commit rồi, nên hãy kiểm tra trước nếu bạn lo ngại.

Chạy PRAGMA foreign_key_check; sau khi chuyển đổi để xác nhận không còn hàng mồ côi nào.

Một cấu trúc thực tế

Ghép tất cả lại: cấu trúc của một blog nhỏ với bảng cha, bảng con và một bảng trung gian cho quan hệ nhiều-nhiều giữa bài viết và thẻ:

Ba điều cần để ý. author_id là NOT NULL: mọi bài viết đều phải có tác giả. Khóa ngoại posts → authors dùng cascade, nên xóa một tác giả sẽ xóa sạch bài viết của họ. Bảng trung gian post_tags dùng cascade ở cả hai phía, nên xóa một bài viết hay một thẻ đều tự động dọn các hàng liên kết.

Thói quen giúp bạn đỡ khổ về sau

  • Đặt PRAGMA foreign_keys = ON; trên mọi kết nối. Đưa nó vào quy trình mở cơ sở dữ liệu của ứng dụng, đừng trông vào trí nhớ.
  • Thêm index trên cột khóa ngoại. SQLite tự đánh index cho khóa của bảng cha nhưng không làm vậy cho bảng con, và ON DELETE CASCADE phải tra cứu bảng con mỗi lần hàng cha bị xóa.
  • Chọn ON DELETE có chủ đích. Giá trị mặc định (NO ACTION) an toàn nhưng nghĩa là bạn sẽ gặp lỗi "constraint failed" mỗi lần dọn dữ liệu. Hãy quyết định điều nên xảy ra và khai báo nó.
  • Chạy PRAGMA foreign_key_check; sau khi chuyển đổi cấu trúc hoặc nhập dữ liệu hàng loạt để phát hiện hàng mồ côi trước khi chúng thành bug.

Tiếp theo: INNER JOIN

Khóa ngoại mô tả mối quan hệ; join là cách bạn thực sự truy vấn qua mối quan hệ đó. Trang tiếp theo nói về INNER JOIN: kết hợp các hàng từ những bảng liên quan và lấy về những cột bạn cần từ mỗi bảng.

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

Làm sao tạo khóa ngoại trong SQLite?

Thêm mệnh đề REFERENCES other_table(column) vào định nghĩa cột trong CREATE TABLE. Ví dụ, author_id INTEGER REFERENCES authors(id) khiến author_id trỏ đến một hàng trong authors. Cột được tham chiếu phải là PRIMARY KEY hoặc có ràng buộc UNIQUE.

Vì sao khóa ngoại trong SQLite không được kiểm tra?

SQLite đọc hiểu khai báo khóa ngoại nhưng không kiểm tra chúng trừ khi bạn bật chế độ kiểm tra. Chạy PRAGMA foreign_keys = ON; ở đầu mỗi kết nối. Thiết lập này áp dụng theo từng kết nối, không lưu trong cơ sở dữ liệu, nên thư viện và CLI phải đặt lại mỗi lần kết nối.

ON DELETE CASCADE trong SQLite làm gì?

ON DELETE CASCADE bảo SQLite tự động xóa các hàng con khi hàng cha của chúng bị xóa. Các lựa chọn khác là RESTRICT (chặn lệnh xóa), SET NULL (gán null cho cột khóa ngoại), SET DEFAULT và NO ACTION (mặc định, trên thực tế giống RESTRICT). Hãy chọn dựa trên việc các hàng con còn ý nghĩa hay không khi không có hàng cha.

Làm sao sửa lỗi 'foreign key constraint failed' trong SQLite?

Lỗi này nghĩa là bạn đã thêm hoặc cập nhật một hàng có giá trị khóa ngoại không khớp với hàng nào trong bảng được tham chiếu, hoặc bạn đã xóa một hàng cha vẫn còn hàng con. Hãy kiểm tra hàng được tham chiếu đã tồn tại trước, hoặc thiết lập ON DELETE CASCADE nếu bạn muốn các hàng con tự động bị xóa.

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