1. 10:37 01/09/2026
BỘ DATABASE BLUEPRINT — WEB/APP DEVELOPMENT (MySQL 8.x/9.x)
Mỗi bài là một schema khởi đầu có thể copy–dán và tùy biến. Trước production, hãy dùng migration có version, user database tối thiểu quyền, sao lưu có kiểm tra khôi phục và đánh giá bảo mật/pháp lý. Không lưu mật khẩu, token đăng nhập hay khóa API ở dạng thô.
# 03/10 — Schema cho mạng xã hội hoặc cộng đồng
Mô hình này dùng cho forum hiện đại, cộng đồng nội bộ hoặc ứng dụng có feed. Những phần cần thiết kế từ đầu là quyền xem, moderation/soft-delete, chống reaction trùng, thông báo và báo cáo nội dung. Không đưa dữ liệu hồ sơ nhạy cảm vào bảng `users` nếu nó không cần cho trải nghiệm cộng đồng.
## Quan hệ chính
```text
users ─1─1 profiles
users ─< posts ─< comments
users >─< users (follows)
users >─< posts/comments (reactions)
users ─< notifications
users ─< content_reports >─ posts/comments
```
## Schema SQL
```sql
CREATE DATABASE IF NOT EXISTS community_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE community_app;
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
username VARCHAR(50) NOT NULL,
status ENUM('active','restricted','suspended','deleted') NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
UNIQUE KEY uq_users_email (email),
UNIQUE KEY uq_users_username (username),
KEY idx_users_status_created (status, created_at)
) ENGINE=InnoDB;
CREATE TABLE profiles (
user_id BIGINT UNSIGNED PRIMARY KEY,
display_name VARCHAR(100) NOT NULL,
bio VARCHAR(500) NULL,
avatar_path VARCHAR(500) NULL,
visibility ENUM('public','members','private') NOT NULL DEFAULT 'public',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_profiles_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE follows (
follower_id BIGINT UNSIGNED NOT NULL,
followed_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (follower_id, followed_id),
CONSTRAINT fk_follows_follower FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_follows_followed FOREIGN KEY (followed_id) REFERENCES users(id) ON DELETE CASCADE,
CHECK (follower_id <> followed_id),
KEY idx_follows_followed_created (followed_id, created_at)
) ENGINE=InnoDB;
CREATE TABLE posts (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
author_id BIGINT UNSIGNED NOT NULL,
body TEXT NOT NULL,
visibility ENUM('public','followers','private') NOT NULL DEFAULT 'public',
status ENUM('published','hidden','deleted') NOT NULL DEFAULT 'published',
reply_to_post_id BIGINT UNSIGNED NULL,
comments_count INT UNSIGNED NOT NULL DEFAULT 0,
reactions_count INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
CONSTRAINT fk_posts_author FOREIGN KEY (author_id) REFERENCES users(id),
CONSTRAINT fk_posts_parent FOREIGN KEY (reply_to_post_id) REFERENCES posts(id) ON DELETE SET NULL,
KEY idx_posts_feed (status, visibility, created_at, id),
KEY idx_posts_author_created (author_id, created_at),
KEY idx_posts_parent_created (reply_to_post_id, created_at)
) ENGINE=InnoDB;
CREATE TABLE comments (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
post_id BIGINT UNSIGNED NOT NULL,
author_id BIGINT UNSIGNED NOT NULL,
parent_comment_id BIGINT UNSIGNED NULL,
body TEXT NOT NULL,
status ENUM('published','hidden','deleted') NOT NULL DEFAULT 'published',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_comments_post FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
CONSTRAINT fk_comments_author FOREIGN KEY (author_id) REFERENCES users(id),
CONSTRAINT fk_comments_parent FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE SET NULL,
KEY idx_comments_post_created (post_id, status, created_at),
KEY idx_comments_author_created (author_id, created_at)
) ENGINE=InnoDB;
CREATE TABLE reactions (
user_id BIGINT UNSIGNED NOT NULL,
target_type ENUM('post','comment') NOT NULL,
target_id BIGINT UNSIGNED NOT NULL,
reaction_type ENUM('like','love','helpful','laugh') NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, target_type, target_id),
CONSTRAINT fk_reactions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
KEY idx_reactions_target (target_type, target_id, reaction_type)
) ENGINE=InnoDB;
CREATE TABLE notifications (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
actor_id BIGINT UNSIGNED NULL,
type VARCHAR(60) NOT NULL,
entity_type VARCHAR(60) NULL,
entity_id BIGINT UNSIGNED NULL,
payload_json JSON NULL,
read_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_notifications_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_notifications_actor FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL,
KEY idx_notifications_unread (user_id, read_at, created_at)
) ENGINE=InnoDB;
CREATE TABLE content_reports (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
reporter_id BIGINT UNSIGNED NOT NULL,
target_type ENUM('post','comment','user') NOT NULL,
target_id BIGINT UNSIGNED NOT NULL,
reason_code VARCHAR(60) NOT NULL,
detail VARCHAR(1000) NULL,
status ENUM('open','reviewing','resolved','rejected') NOT NULL DEFAULT 'open',
reviewer_id BIGINT UNSIGNED NULL,
resolution_note VARCHAR(1000) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME NULL,
CONSTRAINT fk_reports_reporter FOREIGN KEY (reporter_id) REFERENCES users(id),
CONSTRAINT fk_reports_reviewer FOREIGN KEY (reviewer_id) REFERENCES users(id) ON DELETE SET NULL,
KEY idx_reports_status_created (status, created_at),
KEY idx_reports_target (target_type, target_id)
) ENGINE=InnoDB;
```
## Quy tắc ở service layer
- Khóa chính `reactions(user_id, target_type, target_id)` ngăn reaction trùng. Vì FK không thể trỏ đồng thời tới `posts` và `comments`, service phải kiểm tra mục tiêu tồn tại và quyền xem trước khi insert.
- Xóa nội dung thường là đổi `status='deleted'`, che body khỏi người dùng thường và lưu dấu vết moderation. Chỉ xóa cứng theo chính sách lưu trữ đã phê duyệt.
- Feed phải lọc status **và** visibility/quan hệ follow tại server; không dựa vào CSS ở client để “ẩn” dữ liệu.
- Khi cập nhật `comments_count`/`reactions_count`, làm trong cùng transaction với insert/delete; định kỳ đối soát lại bằng `COUNT(*)`.
Bài 04 thiết kế CMS/blog có bản nháp, revision, taxonomy và media.