Thiết Kế Schema Database — Từ Yêu Cầu Đến Bảng

Giới Thiệu
Bạn cần thêm ba bảng mới cho tính năng mới. Tên bảng nên số nhiều hay số ít? Có cần created_at cho mọi bảng không? Dùng UUID hay auto-increment ID? Những quyết định nhỏ này tích lũy nhanh chóng — schema thiết kế kém dẫn đến query chậm, data anomalies, và tính năng khó xây dựng.
Bài viết này hướng dẫn cách tiếp cận có hệ thống cho thiết kế schema — từ thu thập yêu cầu đến tạo bảng production-ready, với hai case study thực tế.
Điều kiện tiên quyết: Bài viết này dựa trên Cơ Bản Về Cơ Sở Dữ Liệu Quan Hệ. Bạn nên hiểu tables, primary keys, foreign keys, constraints, và data types trước khi tiếp tục.
Bạn Sẽ Học Được Gì
✅ Thu thập yêu cầu và xác định entities từ business rules
✅ Thiết kế relationships one-to-one, one-to-many, và many-to-many
✅ Áp dụng normalization (1NF, 2NF, 3NF) với ví dụ thực tế
✅ Biết khi nào denormalize — và tại sao đó là chiến lược, không phải lười biếng
✅ Chọn naming conventions nhất quán cho toàn bộ codebase
✅ Triển khai soft deletes, audit columns, và timestamps
✅ Quyết định giữa UUIDs và auto-increment IDs
✅ Sử dụng JSON/JSONB columns một cách chiến lược
✅ Lên kế hoạch schema evolution và backward compatibility
✅ Thiết kế hai schema thực tế: blog platform và SaaS multi-tenant
Từ Yêu Cầu Đến Entities
Trích Xuất Entities và Relationships
Đọc yêu cầu và tìm danh từ (entities) và động từ (relationships):
Yêu cầu: "Users viết blog posts. Mỗi post thuộc một category
nhưng có thể có nhiều tags. Users có thể comment trên posts.
Posts có thể published hoặc draft."Entities: Users, Posts, Categories, Tags, Comments
Relationships:
- User → Post (one-to-many)
- Category → Post (one-to-many)
- Post ↔ Tag (many-to-many)
- Post → Comment (one-to-many)
Entity vs Attribute
| Trường hợp | Entity hay Attribute? | Lý do |
|---|---|---|
| Email người dùng | Attribute | Giá trị đơn giản, không có relationships |
| Địa chỉ | Tùy trường hợp | Tái sử dụng → entity. Duy nhất per user → attribute |
| Category sản phẩm | Entity | Có attributes riêng, được query độc lập |
| Trạng thái đơn hàng | Attribute (enum) | Tập giá trị cố định |
Quy tắc: Nếu có attributes riêng, được query độc lập, hoặc tham gia relationships — tạo bảng. Nếu là giá trị đơn giản chỉ có ý nghĩa trong context parent — tạo column.
Relationships Deep Dive
One-to-One
Dùng khi tách dữ liệu nhạy cảm, chia bảng lớn, hoặc thêm extension tùy chọn:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE profiles (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
bio TEXT,
avatar_url VARCHAR(500)
);UNIQUE trên user_id đảm bảo mỗi user chỉ có một profile.
One-to-Many
Relationship phổ biến nhất. Foreign key nằm ở bên "many":
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL
);
-- ❌ KHÔNG NÊN: Lưu nhiều IDs trong array
-- ALTER TABLE users ADD COLUMN post_ids INT[];
-- ✅ NÊN: Foreign key ở bảng "many" (như trên)Many-to-Many
Cần junction table (bảng trung gian):
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
slug VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE post_tags (
post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id INT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);Junction table với attributes — khi relationship có dữ liệu riêng:
CREATE TABLE enrollments (
user_id INT NOT NULL REFERENCES users(id),
course_id INT NOT NULL REFERENCES courses(id),
enrolled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
grade DECIMAL(5, 2) CHECK (grade >= 0 AND grade <= 100),
PRIMARY KEY (user_id, course_id)
);Self-Referencing Relationships
-- Categories phân cấp
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
parent_id INT REFERENCES categories(id) ON DELETE SET NULL
);Normalization: 1NF, 2NF, 3NF
Normalization tổ chức dữ liệu để giảm redundancy và ngăn anomalies. Mục tiêu: mỗi fact được lưu đúng một lần.
First Normal Form (1NF)
Quy tắc: Mỗi column chứa giá trị nguyên tử. Không có repeating groups.
-- ❌ Vi phạm 1NF: Giá trị phân tách bằng dấu phẩy
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
products VARCHAR(500) -- "Laptop, Mouse, Keyboard"
);
-- ❌ Vi phạm 1NF: Columns lặp lại
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
product_1 VARCHAR(100),
product_2 VARCHAR(100),
product_3 VARCHAR(100) -- Nếu 4 sản phẩm thì sao?
);
-- ✅ Thỏa 1NF: Bảng riêng cho order items
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id INT NOT NULL REFERENCES products(id),
quantity INT NOT NULL CHECK (quantity > 0)
);Second Normal Form (2NF)
Quy tắc: 1NF + mọi non-key column phụ thuộc vào toàn bộ primary key.
-- ❌ Vi phạm 2NF: product_name chỉ phụ thuộc vào product_id
CREATE TABLE order_items (
order_id INT NOT NULL,
product_id INT NOT NULL,
product_name VARCHAR(200), -- Partial dependency!
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);
-- ✅ Thỏa 2NF: Product data trong bảng riêng
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(10, 2) NOT NULL
);
CREATE TABLE order_items (
order_id INT NOT NULL REFERENCES orders(id),
product_id INT NOT NULL REFERENCES products(id),
quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL, -- Snapshot giá tại thời điểm mua
PRIMARY KEY (order_id, product_id)
);Third Normal Form (3NF)
Quy tắc: 2NF + không có transitive dependencies.
-- ❌ Vi phạm 3NF: country phụ thuộc vào zip_code, không phải customer id
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
zip_code VARCHAR(20),
city VARCHAR(100),
country VARCHAR(100) -- Phụ thuộc vào zip_code
);
-- ✅ Thỏa 3NF: Tách location data
CREATE TABLE zip_codes (
zip_code VARCHAR(20) PRIMARY KEY,
city VARCHAR(100) NOT NULL,
country VARCHAR(100) NOT NULL
);
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
zip_code VARCHAR(20) REFERENCES zip_codes(zip_code)
);Với hầu hết ứng dụng, 3NF là mục tiêu phù hợp. Bắt đầu ở 3NF, sau đó denormalize khi đo được vấn đề hiệu suất cụ thể.
Khi Nào Denormalize
Denormalization thêm redundancy có chủ ý để cải thiện hiệu suất đọc. Đó là trade-off có chủ đích, không phải lười biếng.
Pattern 1: Cache Aggregates
-- Thay vì đếm posts mỗi lần load trang:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
post_count INT NOT NULL DEFAULT 0 -- Denormalized
);
-- Giữ đồng bộ bằng trigger:
CREATE OR REPLACE FUNCTION update_user_post_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE users SET post_count = post_count + 1 WHERE id = NEW.user_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE users SET post_count = post_count - 1 WHERE id = OLD.user_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;Pattern 2: Snapshot Historical Data
-- Order items lưu thông tin product tại thời điểm mua
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INT NOT NULL REFERENCES orders(id),
product_id INT NOT NULL REFERENCES products(id),
product_name VARCHAR(200) NOT NULL, -- Snapshot
unit_price DECIMAL(10, 2) NOT NULL, -- Snapshot
quantity INT NOT NULL
);Bảng Quyết Định Denormalize
| Trường hợp | Denormalize? | Lý do |
|---|---|---|
| Dữ liệu thay đổi thường xuyên | ❌ Không | Khó giữ đồng bộ |
| Bảng write-heavy | ❌ Không | Overhead update quá cao |
| Bảng nhỏ (< 10k rows) | ❌ Không | Joins đã đủ nhanh |
| Dashboard reporting | ✅ Có | Chấp nhận dữ liệu hơi cũ |
| Lịch sử đơn hàng | ✅ Có | Cần snapshot thời điểm |
Naming Conventions
| Phần tử | Convention | Ví dụ |
|---|---|---|
| Tên bảng | Số nhiều, snake_case | users, order_items |
| Tên column | snake_case | first_name, created_at |
| Primary keys | id | id BIGSERIAL PRIMARY KEY |
| Foreign keys | {table_singular}_id | user_id, category_id |
| Booleans | Prefix is_ hoặc has_ | is_active, has_paid |
| Timestamps | Suffix _at | created_at, deleted_at |
| Indexes | idx_{table}_{columns} | idx_posts_user_id |
-- ✅ NÊN
users, blog_posts, order_items
user_id, created_at, is_active
-- ❌ KHÔNG NÊN
User, blogPost, tbl_users
userId, fName, activeSoft Deletes vs Hard Deletes
Hard Delete
DELETE FROM users WHERE id = 1; -- Xóa vĩnh viễnDùng khi: GDPR, dữ liệu tạm thời, không cần audit trail.
Soft Delete
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
deleted_at TIMESTAMPTZ -- NULL = active, timestamp = đã xóa
);
UPDATE users SET deleted_at = NOW() WHERE id = 1; -- "Xóa"
SELECT * FROM users WHERE deleted_at IS NULL; -- Query active users
UPDATE users SET deleted_at = NULL WHERE id = 1; -- Khôi phụcDùng khi: Audit trails, undo/restore, compliance, analytics.
Quan trọng: Nhớ thêm WHERE deleted_at IS NULL vào mọi query. Cân nhắc tạo view:
CREATE VIEW active_users AS
SELECT * FROM users WHERE deleted_at IS NULL;Audit Columns
Thêm tracking columns vào mọi bảng business data:
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
-- Audit columns
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Trigger auto-update updated_at
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;| Column | Bắt buộc? | Khi nào dùng |
|---|---|---|
created_at | ✅ Luôn | Mọi bảng |
updated_at | ✅ Luôn | Mọi bảng mutable |
created_by | Nên có | Hệ thống multi-user |
deleted_at | Tùy chọn | Chỉ khi soft delete |
UUIDs vs Auto-Increment IDs
Auto-Increment
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- 1, 2, 3, ...
email VARCHAR(255) NOT NULL
);Ưu: Nhỏ (8 bytes), insert nhanh, dễ đọc, index hiệu quả. Nhược: Dự đoán được (security risk), conflict khi merge databases.
UUIDs
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL
);Ưu: Globally unique, không dự đoán được, generate client-side. Nhược: Lớn (16 bytes), insert chậm hơn, khó debug.
Hybrid Approach (Khuyến nghị)
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- Internal
public_id UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(), -- API
email VARCHAR(255) NOT NULL UNIQUE
);
-- API trả public_id, database join dùng id (integer nhanh hơn)| Use Case | Auto-Increment | UUID |
|---|---|---|
| Bảng internal | ✅ | |
| Public API IDs | ✅ | |
| Distributed systems | ✅ | |
| High-write tables | ✅ |
JSON/JSONB Columns
Khi Nào Dùng JSONB
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
attributes JSONB DEFAULT '{}'::JSONB -- Khác nhau per product type
);
-- Laptop: {"ram_gb": 16, "cpu": "Intel i7"}
-- Áo: {"size": "L", "color": "blue"}Dùng cho: Variable attributes, user preferences, API responses.
Khi Nào KHÔNG Dùng JSONB
-- ❌ Core business data trong JSONB
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
data JSONB -- Không constraints, không FK, không type safety
);
-- ✅ Schema quan hệ đúng cách
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(id),
total DECIMAL(10, 2) NOT NULL CHECK (total >= 0)
);Schema Evolution
Safe Migrations
-- ✅ AN TOÀN: Thêm nullable column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- ✅ AN TOÀN: Thêm column có default
ALTER TABLE users ADD COLUMN is_verified BOOLEAN NOT NULL DEFAULT FALSE;
-- ❌ NGUY HIỂM: NOT NULL không có default
ALTER TABLE users ADD COLUMN required_field VARCHAR(100) NOT NULL;
-- ERROR: contains null valuesMulti-Step Migration
-- 1. Thêm nullable
ALTER TABLE users ADD COLUMN email_verified BOOLEAN;
-- 2. Backfill
UPDATE users SET email_verified = FALSE WHERE email_verified IS NULL;
-- 3. Thêm NOT NULL
ALTER TABLE users ALTER COLUMN email_verified SET NOT NULL;
-- 4. Thêm default
ALTER TABLE users ALTER COLUMN email_verified SET DEFAULT FALSE;Case Study 1: Blog Platform
-- Users
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Categories (phân cấp)
CREATE TABLE categories (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
parent_id BIGINT REFERENCES categories(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Tags
CREATE TABLE tags (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
slug VARCHAR(50) NOT NULL UNIQUE
);
-- Posts
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
category_id BIGINT REFERENCES categories(id) ON DELETE SET NULL,
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) NOT NULL UNIQUE,
excerpt TEXT,
content TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'archived')),
published_at TIMESTAMPTZ,
view_count INT NOT NULL DEFAULT 0,
comment_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK (status != 'published' OR published_at IS NOT NULL)
);
-- Post ↔ Tags
CREATE TABLE post_tags (
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id BIGINT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
-- Comments (nested)
CREATE TABLE comments (
id BIGSERIAL PRIMARY KEY,
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id),
parent_id BIGINT REFERENCES comments(id) ON DELETE CASCADE,
content TEXT NOT NULL,
is_approved BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_published_at ON posts(published_at DESC);
CREATE INDEX idx_comments_post_id ON comments(post_id);Quyết định thiết kế: Categories phân cấp qua parent_id, counters denormalized (view_count, comment_count), CHECK constraint đảm bảo published posts có published_at, comments nested qua self-referencing FK.
Case Study 2: SaaS Multi-Tenant
-- Organizations (tenants)
CREATE TABLE organizations (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
slug VARCHAR(200) NOT NULL UNIQUE,
plan VARCHAR(20) NOT NULL DEFAULT 'free'
CHECK (plan IN ('free', 'pro', 'enterprise')),
max_users INT NOT NULL DEFAULT 5,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Users (global)
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Memberships (many-to-many with roles)
CREATE TABLE organization_members (
organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL DEFAULT 'member'
CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (organization_id, user_id)
);
-- Projects
CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY,
organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name VARCHAR(200) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'archived', 'completed')),
created_by BIGINT NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (organization_id, name)
);
-- Tasks
CREATE TABLE tasks (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
organization_id BIGINT NOT NULL REFERENCES organizations(id),
title VARCHAR(200) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'todo'
CHECK (status IN ('todo', 'in_progress', 'review', 'done', 'cancelled')),
priority VARCHAR(20) NOT NULL DEFAULT 'medium'
CHECK (priority IN ('low', 'medium', 'high', 'urgent')),
assigned_to BIGINT REFERENCES users(id),
due_date DATE,
created_by BIGINT NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_org_members_user ON organization_members(user_id);
CREATE INDEX idx_projects_org ON projects(organization_id);
CREATE INDEX idx_tasks_org ON tasks(organization_id);
CREATE INDEX idx_tasks_assigned ON tasks(assigned_to);Row-Level Security
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
CREATE POLICY tasks_tenant_isolation ON tasks
USING (
organization_id IN (
SELECT organization_id FROM organization_members
WHERE user_id = current_setting('app.current_user_id')::BIGINT
)
);Quyết định thiết kế: organization_id denormalized trong tasks cho RLS hiệu quả, composite UNIQUE trên projects cho phép tên trùng giữa các orgs, users global có thể thuộc nhiều orgs, RLS ngăn data leak ở database level.
Tổng Kết
✅ Bắt đầu từ yêu cầu — Trích xuất entities và relationships từ business rules
✅ Normalize đến 3NF — Mỗi fact lưu một lần, không partial hay transitive dependencies
✅ Denormalize có chiến lược — Cache aggregates, snapshot historical data, materialized views
✅ Naming nhất quán — Plural tables, snake_case columns, {table}_id cho FKs
✅ Audit columns mọi nơi — created_at, updated_at mọi bảng; deleted_at cho soft deletes
✅ Chọn IDs phù hợp — BIGSERIAL internal, UUID cho public API
✅ JSONB cho variable data — Product attributes, preferences; không cho core business data
✅ Lên kế hoạch evolution — Nullable trước, backfill, rồi constraints
✅ Multi-tenant cần isolation — Denormalize org_id, dùng Row-Level Security
✅ Index mọi foreign key — FK không index gây slow joins và cascades
Tiếp Theo
📚 Cơ Bản Về CSDL Quan Hệ — Ôn lại relational model, ACID, data types, constraints
📚 SQL Indexing Deep Dive (sắp ra) — B-tree indexes, composite indexes, EXPLAIN ANALYZE
📚 SQL Query Optimization Guide (sắp ra) — N+1 problems, JOIN strategies, pagination, profiling
Kết Luận
Schema design vừa là nghệ thuật vừa là khoa học. Normalization, constraints, data types là khoa học. Biết khi nào phá vỡ quy tắc — denormalization, JSONB, composite keys — là nghệ thuật.
Bắt đầu normalized. Denormalize chỉ khi đo được vấn đề hiệu suất thực sự. Dùng naming conventions nhất quán từ ngày đầu. Luôn thêm audit columns — bạn sẽ cảm ơn mình khi debug lúc 2 giờ sáng.
Schema là nền tảng của ứng dụng. Thiết kế đúng, mọi thứ khác sẽ dễ dàng hơn.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.