Giao diện
Data Integrity: Constraints & Foreign Keys (Defense in Depth)
Dữ liệu rác vào, rác ra. "Garbage In, Garbage Out." Bài học này sẽ dạy bạn cách biến Database thành lớp phòng thủ cuối cùng cho chất lượng dữ liệu.
1️⃣ Concept: Các loại Constraints
Constraint là các quy tắc mà Database tự động kiểm tra MỖI KHI bạn INSERT hoặc UPDATE dữ liệu. Nếu vi phạm -> Database sẽ reject ngay lập tức.
PRIMARY KEY (Khóa chính)
- Định nghĩa: Định danh duy nhất cho mỗi dòng trong bảng.
- Quy tắc: Không được
NULL, không được trùng lặp (UNIQUE). - Thực tế: Thường dùng
idvới kiểuSERIAL(PostgreSQL) hoặcAUTO_INCREMENT(MySQL).
sql
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- PostgreSQL
-- id INT AUTO_INCREMENT PRIMARY KEY, -- MySQL
email VARCHAR(255) NOT NULL
);FOREIGN KEY (Khóa ngoại) - Referential Integrity
- Định nghĩa: Đảm bảo rằng một giá trị trong bảng này phải tồn tại ở bảng khác.
- Mục đích: Ngăn chặn dữ liệu "mồ côi" (orphan records). Ví dụ: Đơn hàng của User không tồn tại.
sql
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Nếu INSERT order với user_id = 999 nhưng user 999 không tồn tại -> BÁO LỖI!UNIQUE
- Định nghĩa: Đảm bảo giá trị trong cột là duy nhất trong toàn bộ bảng.
- Khác với PRIMARY KEY: Có thể có nhiều UNIQUE constraint, và cột UNIQUE có thể chứa
NULL(tùy DB).
sql
ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);CHECK
- Định nghĩa: Kiểm tra điều kiện tùy ý trước khi chấp nhận dữ liệu.
- Ví dụ: Tuổi phải >= 18, số lượng sản phẩm phải dương.
sql
ALTER TABLE users ADD CONSTRAINT check_adult CHECK (age >= 18);
-- INSERT INTO users (name, age) VALUES ('Kid', 10); -> LỖI!NOT NULL
- Định nghĩa: Bắt buộc cột phải có giá trị, không được để trống.
sql
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, -- Tên sản phẩm bắt buộc
description TEXT -- Mô tả thì có thể NULL
);2️⃣ The "Why": Tại sao validate ở Database, không chỉ Backend?
Đây là nguyên tắc Defense in Depth (Phòng thủ theo chiều sâu).
┌──────────────────────────────────────────────────────────────┐
│ USER INPUT │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 🛡️ TẦNG 1: FRONTEND VALIDATION │
│ (JavaScript, Form rules) │
│ → Dễ bị bypass bằng DevTools, API clients │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 🛡️🛡️ TẦNG 2: BACKEND VALIDATION │
│ (API, Service Layer) │
│ → Có thể bị bypass nếu có bug, hoặc ai đó truy cập trực │
│ tiếp vào DB (dev, admin, migration script) │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 🛡️🛡️🛡️ TẦNG 3: DATABASE CONSTRAINTS │
│ (Primary Key, Foreign Key, Check, Unique, Not Null) │
│ → Lớp phòng thủ CUỐI CÙNG. KHÔNG THỂ BYPASS. Đây là │
│ "Source of Truth" và là hàng rào sắt cho dữ liệu. │
└──────────────────────────────────────────────────────────────┘Câu chuyện thực tế
Một developer tự xóa user bằng lệnh SQL trực tiếp trên production DB để "sửa lỗi". Anh ta quên rằng user đó còn 500 đơn hàng. Kết quả: 500 "ghost orders" không có thông tin khách hàng, báo cáo tài chính sai, hệ thống crash. Nếu có ON DELETE RESTRICT, Database đã ngăn chặn hành động này.
3️⃣ Production Code: Schema với Strict Constraints
Đây là schema mẫu cho hệ thống E-commerce, được thiết kế để không bao giờ nhận dữ liệu bẩn.
sql
-- ================================================================
-- BẢNG USERS: Thông tin người dùng
-- ================================================================
CREATE TABLE users (
id SERIAL PRIMARY KEY,
-- NOT NULL: Các trường bắt buộc
email VARCHAR(255) NOT NULL,
full_name VARCHAR(100) NOT NULL,
age INT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
-- Timestamps với default values
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- UNIQUE: Email không được trùng
CONSTRAINT unique_user_email UNIQUE (email),
-- CHECK: Tuổi phải >= 18 (theo quy định pháp luật)
CONSTRAINT check_user_adult CHECK (age >= 18),
-- CHECK: Status chỉ được là các giá trị hợp lệ
CONSTRAINT check_user_status CHECK (status IN ('active', 'inactive', 'banned'))
);
-- Index cho tìm kiếm nhanh theo email
CREATE INDEX idx_users_email ON users(email);
-- ================================================================
-- BẢNG ORDERS: Đơn hàng của người dùng
-- ================================================================
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
-- NOT NULL: Đơn hàng phải thuộc về một user
user_id INT NOT NULL,
-- Thông tin đơn hàng
order_code VARCHAR(50) NOT NULL,
total_amount DECIMAL(15, 2) NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'pending',
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- FOREIGN KEY: Đảm bảo user_id phải tồn tại trong bảng users
-- ON DELETE RESTRICT: Không cho xóa user nếu còn đơn hàng
CONSTRAINT fk_orders_user FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
-- UNIQUE: Mã đơn hàng không được trùng
CONSTRAINT unique_order_code UNIQUE (order_code),
-- CHECK: Số tiền phải dương
CONSTRAINT check_positive_amount CHECK (total_amount > 0),
-- CHECK: Status hợp lệ
CONSTRAINT check_order_status CHECK (
status IN ('pending', 'confirmed', 'shipping', 'completed', 'cancelled')
)
);
-- Index cho query theo user
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_created_at ON orders(created_at);4️⃣ Deep Dive: ON DELETE CASCADE vs ON DELETE SET NULL
Khi bạn xóa một dòng ở bảng cha (ví dụ: users), database sẽ làm gì với các dòng con liên quan (ví dụ: orders)?
Các tùy chọn ON DELETE:
| Hành động | Mô tả | Ví dụ thực tế |
|---|---|---|
RESTRICT (Default) | Chặn xóa. Nếu còn dòng con -> Lỗi. | User còn đơn hàng -> Không cho xóa user. |
CASCADE | Xóa theo. Xóa cha -> Tự động xóa hết tất cả con. | Xóa user -> Tự động xóa tất cả đơn hàng của user đó. |
SET NULL | Đặt NULL. Xóa cha -> user_id của con thành NULL. | Xóa user -> Đơn hàng vẫn còn, nhưng user_id = NULL. |
SET DEFAULT | Đặt giá trị mặc định. | Ít dùng trong thực tế. |
NO ACTION | Tương tự RESTRICT, nhưng kiểm tra trì hoãn. | Dùng cho deferred constraints. |
So sánh CASCADE vs SET NULL:
sql
-- Option 1: ON DELETE CASCADE
-- Xóa user_id = 1 => TỰ ĐỘNG xóa tất cả orders có user_id = 1
CONSTRAINT fk_orders_user FOREIGN KEY (user_id)
REFERENCES users(id) ON DELETE CASCADE;
-- Option 2: ON DELETE SET NULL (cột phải cho phép NULL)
-- Xóa user_id = 1 => Tất cả orders có user_id = 1 sẽ thành user_id = NULL
ALTER TABLE orders ALTER COLUMN user_id DROP NOT NULL; -- Cho phép NULL trước
CONSTRAINT fk_orders_user FOREIGN KEY (user_id)
REFERENCES users(id) ON DELETE SET NULL;⚠️ Rủi ro chết người của ON DELETE CASCADE
CẢNH BÁO: Historical Data Loss
CASCADE cực kỳ nguy hiểm trong các hệ thống cần lưu trữ lịch sử (Auditing, Compliance).
Ví dụ thảm họa:
- Bạn có bảng
usersvàorders. - Một user yêu cầu xóa tài khoản (GDPR).
- Bạn
DELETE FROM users WHERE id = 1. CASCADEtự động xóa TẤT CẢ 500 đơn hàng của user đó.- Kết quả: Dữ liệu tài chính sai, báo cáo doanh thu tháng đó lệch 50 triệu, bộ phận kế toán gọi điện hỏi thăm.
Giải pháp:
- Dùng
RESTRICTđể ngăn xóa. - Thay vì xóa cứng (Hard Delete), hãy Soft Delete bằng cách đặt cờ
is_deleted = truehoặcdeleted_at = NOW().
💡 HPN Golden Rule cho Production
Nguyên tắc:
ON DELETE RESTRICTlà mặc định an toàn nhất cho mọi hệ thống.ON DELETE CASCADEchỉ nên dùng cho các bảng phụ trợ (Config, Temp, Log tạm thời), KHÔNG BAO GIỜ cho dữ liệu core (Users, Orders, Transactions).ON DELETE SET NULLhữu ích khi bạn muốn giữ lại record con nhưng chấp nhận mất liên kết (ví dụ: user rời đi nhưng đơn hàng vẫn cần cho báo cáo).
Tổng kết
| Constraint | Mục đích | Ví dụ |
|---|---|---|
PRIMARY KEY | Định danh duy nhất | id SERIAL PRIMARY KEY |
FOREIGN KEY | Đảm bảo tham chiếu hợp lệ | REFERENCES users(id) |
UNIQUE | Không trùng lặp | UNIQUE (email) |
CHECK | Điều kiện tùy chỉnh | CHECK (age >= 18) |
NOT NULL | Bắt buộc có giá trị | name VARCHAR NOT NULL |
💡 HPN Pro Tip: Đừng sợ "strict"
Nhiều junior developer ngại đặt quá nhiều constraints vì "sợ lỗi". Nhưng thực tế, lỗi sớm tốt hơn lỗi muộn. Một lỗi CHECK constraint violation lúc INSERT còn dễ debug hơn việc phát hiện dữ liệu bẩn sau 6 tháng trong production và phải viết script migration để sửa.