A
Đăng nhập

Nội dung này đã khóa

Vui lòng đăng nhập hoặc đăng ký mua sách để đọc tiếp những kiến thức chuyên sâu này.

Mua bản đầy đủ

Chương 7: Database - Tư duy dữ liệu & Thiết kế Schema cho Website bán hàng

Chapter 7 Database

"Nếu coi Website là cơ thể, thì Database là bộ xương. Xương mà lệch thì người đi cà nhắc. Database mà sai thì App càng sửa càng lỗi."

Trong chương này, chúng ta sẽ không nói lý thuyết suông. Chúng ta sẽ cùng nhau thiết kế Bộ xương sống cho dự án VibeMaterial (Web bán Vật liệu xây dựng) - một hệ thống bao gồm cả Bán hàng (Client), Quản trị (Admin) và Kho (Inventory).

7.1 Sai lầm kinh điển: Tư duy "Bảng tính" (Spreadsheet Mindset)

Trước khi vào thiết kế đúng, hãy nhìn lại cách chúng ta hay làm sai trên AppSheet/Excel.

7.1.1 Kịch bản sai lầm

Bạn tạo một bảng DonHang duy nhất với các cột:

  • Ngay: 20/10/2026
  • KhachHang: Anh Hiền
  • SanPham: Xi măng Hà Tiên
  • SoLuong: 10
  • DonGia: 50,000
  • ThanhTien: 500,000

Vấn đề phát sinh khi mở rộng:

  1. Khách mua 2 món?: Bạn phải tạo thêm dòng mới? Hay thêm cột SanPham2, SoLuong2? -> Dữ liệu bị lặp lại thông tin khách hàng.
  2. Khách đổi số điện thoại?: Bạn phải đi sửa lại 1000 dòng đơn hàng cũ? -> Mất tính toàn vẹn (Integrity).
  3. Báo cáo tồn kho?: Làm sao biết hiện tại còn bao nhiêu bao xi măng nếu chỉ nhìn vào bảng Đơn hàng (Đầu ra) mà không có bảng Nhập hàng (Đầu vào)?

7.2 Thiết kế chuẩn Relational Database (SQL) cho VibeMaterial

Chúng ta sẽ tách dữ liệu thành các bảng chuyên biệt và nối chúng lại với nhau.

Bảng products (Danh mục Sản phẩm)

Hàng hóa là gốc rễ.

CREATE TABLE products (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- Mã số tự nhảy (1, 2, 3...)
  name TEXT NOT NULL, -- Tên: "Xi măng Hà Tiên"
  slug TEXT UNIQUE, -- URL thân thiện: "xi-mang-ha-tien" (SEO)
  price INT NOT NULL, -- Giá bán hiện tại: 50000
  cost_price INT, -- Giá vốn (Chỉ Admin thấy): 40000
  stock_quantity INT DEFAULT 0, -- Tồn kho hiện tại
  category_id INT REFERENCES categories(id), -- Thuộc nhóm nào?
  attributes JSONB, -- { "brand": "Hà Tiên", "weight": "50kg" }
  is_active BOOLEAN DEFAULT true, -- Còn bán không?
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Phân tích sâu:

  • Tai sao dùng JSONB cho attributes?: VLXD rất đa dạng. Xi măng thì có "Hãng", Gạch thì có "Kích thước", Sơn thì có "Màu". Thay vì tạo 100 cột null, ta gom vào JSON. Vừa gọn, vừa search nhanh.

Bảng profiles (Khách hàng & User)

Thay vì lưu tên khách trong đơn hàng, ta lưu vào bảng riêng.

CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users, -- ID từ Supabase Auth
  full_name TEXT,
  phone TEXT,
  address TEXT, -- Địa chỉ mặc định
  role TEXT DEFAULT 'customer' -- 'admin' hoặc 'customer'
);

Bảng orders (Đơn hàng - Master)

Chứa thông tin chung của phiếu mua hàng.

CREATE TABLE orders (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- Mã đơn: 1001
  user_id UUID REFERENCES profiles(id), -- Ai mua?
  status TEXT DEFAULT 'new', -- 'new' -> 'confirmed' -> 'shipping' -> 'done'
  total_amount INT DEFAULT 0, -- Tổng tiền
  payment_method TEXT, -- 'cod' hoặc 'banking'
  shipping_address TEXT, -- Địa chỉ giao hàng (có thể khác địa chỉ nhà)
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Bảng order_items (Chi tiết đơn hàng - Detail)

Đây là bảng quan trọng nhất để giải quyết vấn đề "Mua nhiều món".

CREATE TABLE order_items (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  order_id BIGINT REFERENCES orders(id), -- Thuộc đơn 1001
  product_id BIGINT REFERENCES products(id), -- Mua Xi măng
  quantity INT NOT NULL, -- 10 bao
  price_at_purchase INT NOT NULL -- 50000 (Giá LÚC MUA)
);

Phân tích sâu (Bài học xương máu):

  • Tại sao phải lưu lại price_at_purchase? Sao không join sang bảng products lấy giá?
  • Thực tế: Hôm nay xi măng giá 50k. Tháng sau lên 60k. Nếu bạn chỉ tham chiếu sang bảng Product, đơn hàng cũ sẽ tự động nhảy lên 60k -> Sai lệch doanh thu lịch sử.
  • Nguyên tắc: Giá trong đơn hàng là giá "chốt" tại thời điểm mua (Snapshot), bất biến theo thời gian.

Bảng inventory_logs (Nhật ký kho - Audit)

Đừng bao giờ chỉ dùng cột stock_quantity trong bảng Product rồi cộng trừ thẳng vào đó. Bạn sẽ không biết ai đã lấy mất hàng. Hãy dùng mô hình "Sổ cái" (Ledger).

CREATE TABLE inventory_logs (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  product_id BIGINT REFERENCES products(id),
  change_amount INT NOT NULL, -- Số thay đổi: +100 (Nhập), -10 (Bán)
  reason TEXT, -- 'import', 'sale_order_1001', 'damaged'
  created_by UUID REFERENCES profiles(id), -- Ai làm?
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Cơ chế tự động (Database Trigger): Mỗi khi có dòng mới trong inventory_logs, hệ thống tự động cập nhật lại stock_quantity bên bảng Product. -> Bạn vừa kiểm soát được tồn kho tức thời, vừa truy vết được lịch sử 10 năm trước.

7.3 Quy tắc Vàng: Code Tiếng Anh - Hiển thị Tiếng Việt

Đây là lỗi phổ biến nhất của người Việt dùng AppSheet chuyển sang Code.

  • Tại sao phải dùng Tiếng Anh? (orders thay vì don_hang)
    1. AI hiểu chuẩn hơn: Các mô hình LLM được train bằng Tiếng Anh. Dùng customer_id, AI hiểu ngay quan hệ khóa ngoại. Dùng ma_khach, AI phải đoán.
    2. Tiết kiệm Token: Tiếng Anh ngắn gọn hơn.
    3. Chuẩn hóa: Tất cả thư viện quốc tế đều dùng tiếng Anh.

Khẩu quyết: "Cấu trúc (Structure) là Tiếng Anh. Giao diện (Display) là Tiếng Việt."

7.4 Bản đồ quan hệ (ERD Diagram)

Hình dung hệ thống như sau:

  • 1 Khách (profiles) -> Có nhiều Đơn (orders).
  • 1 Đơn (orders) -> Có nhiều Chi tiết (order_items).
  • 1 Sản phẩm (products) -> Xuất hiện trong nhiều Chi tiết.
  • 1 Sản phẩm -> Có nhiều dòng Nhật ký kho (inventory_logs).

Đây chính là cấu trúc Data chuẩn mực giúp Shopee/Lazada vận hành hàng triệu đơn mỗi ngày mà không vỡ trận.

7.5 Thực hành: Prompt AI tạo Database

Bạn không cần gõ đống SQL trên. Hãy copy prompt này:

Role: Senior Database Architect for E-commerce. Task: Create Database Schema for "VibeMaterial" (Construction Materials Store). Tech: Supabase (PostgreSQL). Tables Needed:

  1. products (Include JSONB for attributes like dimensions, brand).
  2. profiles (Linked to auth.users).
  3. orders & order_items (Master-Detail). Ensure price_at_purchase is stored.
  4. inventory_logs (For audit trail). Requirement:
  • Use standard SQL types (BIGINT, TIMESTAMPTZ).
  • Define Foreign Keys clearly.
  • Add a Trigger: When inventory_logs insert, auto-update products.stock_quantity. Output: Full SQL Script.

Nhờ có "Tư duy thiết kế" này, bạn điều khiển AI tạo ra một bộ Database chuyên nghiệp chỉ trong 30 giây.


7.6 Đúc kết & Giới thiệu chương tiếp theo

Bạn vừa xây xong "Bộ xương sống" cho VibeMaterial - một hệ thống Database chuyên nghiệp với:

  • Tách biệt Master-Detail (orders → order_items)
  • Lưu giá "chốt" tại thời điểm mua (price_at_purchase)
  • Nhật ký kho theo mô hình Sổ cái (inventory_logs)
  • JSONB cho attributes linh hoạt

Khẩu quyết quan trọng: "Cấu trúc là Tiếng Anh. Giao diện là Tiếng Việt."

Database mới chỉ là "Kho lạnh" im lìm. Để nó hoạt động, cần có "Bếp trưởng" điều phối - đó là Backend & API. Ở Chương 8: Backend & API, chúng ta sẽ xây dựng "Hệ thần kinh" để khách bấm "Mua hàng" → hệ thống tự động trừ kho, ghi đơn và báo tin nhắn Telegram về điện thoại cho Sếp.